Observability for Bun with OpenTelemetry

Khimananda Oli 10 min read Programming and Languages
Observability for Bun with OpenTelemetry

By Khimananda Oli | Last reviewed: August 2026

Bun’s raw speed is undeniable, but that performance advantage vanishes if you cannot diagnose failures in production. Implementing observability for Bun with OpenTelemetry requires a different approach than Node.js because Bun uses its own runtime internals and native fetch implementation rather than relying on standard Node polyfills. While early versions required manual patching, Bun 1.2+ includes native OpenTelemetry support that dramatically simplifies instrumentation. This guide walks through the exact configuration needed to capture meaningful signals without degrading the throughput your team chose Bun for.

Bun RuntimeNative Fetch / TCPSQLite / PostgresOTel SDKAuto-InstrumentationContext PropagationOTLP CollectorBatch ProcessingProtocol TranslationBackendTempoPrometheus
Observability for Bun with OpenTelemetry follows a direct pipeline from runtime primitives through the SDK to your observability backend.

How do you configure observability for Bun with OpenTelemetry natively?

Bun distinguishes itself by baking telemetry directly into the runtime binary. Unlike Node.js, where you must monkey-patch core modules at startup, Bun exposes a dedicated command-line flag that initializes the OpenTelemetry stack before any user code executes. This eliminates the race conditions and import-ordering headaches common in traditional setups.

Using the --otel runtime flag

The simplest path to basic tracing is passing the flag at invocation. This enables automatic instrumentation for HTTP server requests, outbound fetch calls, and built-in database drivers like bun:sqlite.

# Start Bun with native OTel enabled
bun --otel run src/server.ts

# Or set via environment variable for Docker/Kubernetes
OTEL_ENABLED=true bun run src/server.ts

When this flag is active, Bun automatically creates spans for incoming HTTP requests and propagates W3C Trace Context headers. However, the native flag alone does not configure an exporter. You must still define where the data goes using standard OTLP environment variables:

  • OTEL_EXPORTER_OTLP_ENDPOINT: Your collector or backend URL (e.g., http://localhost:4318)
  • OTEL_SERVICE_NAME: Logical service identifier for filtering in dashboards
  • OTEL_TRACES_SAMPLER: Sampling strategy (always_on, traceidratio, or parentbased_traceidratio)

Programmatic SDK setup for advanced control

For teams needing custom span processors, metric readers, or log record exporters, the programmatic approach offers full control. Create a dedicated instrumentation.ts file that runs before your application entry point. This pattern aligns with how we instrument applications with OpenTelemetry across other runtimes, but uses Bun-compatible packages.

// instrumentation.ts
import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { BunInstrumentation } from '@opentelemetry/instrumentation-bun-runtime';
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';

const sdk = new NodeSDK({
  serviceName: 'bun-api-service',
  traceExporter: new OTLPTraceExporter({
    url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://localhost:4318/v1/traces',
  }),
  instrumentations: [
    new BunInstrumentation(),
    new HttpInstrumentation(),
  ],
});

sdk.start();

// Graceful shutdown on SIGTERM/SIGINT
process.on('SIGTERM', () => {
  sdk.shutdown().then(
    () => console.log('OTel SDK shut down successfully'),
    (err) => console.error('Error shutting down OTel SDK', err)
  );
});

Load this file using Bun’s --preload flag to ensure it executes before any application code:

bun --preload ./instrumentation.ts run src/server.ts

What are the key differences between Bun and Node.js OpenTelemetry instrumentation?

Engineers migrating from Node.js often assume their existing instrumentation config will transfer directly. It won’t. Bun reimplements core APIs natively, which means Node-specific monkey-patching libraries frequently fail silently or throw errors. Understanding these differences prevents hours of debugging phantom missing spans.

AspectNode.jsBun
HTTP Server InstrumentationPatches http.createServer via require hooksNative hook via BunInstrumentation or --otel flag
Fetch APIRequires third-party polyfill instrumentationBuilt-in global fetch is auto-instrumented natively
Database DriversSeparate instrumentation per driver (pg, mysql2)bun:sqlite native; postgres/mysql via compatible adapters
Module LoadingCommonJS/ESM loader hooksCustom bundler/resolver; preload scripts preferred
Context PropagationAsyncLocalStorage (ALS)Native ALS-compatible context tracking
Cold Start Overhead~50–150ms for SDK init~5–15ms due to compiled-in telemetry primitives

A common mistake is attempting to use @opentelemetry/instrumentation-fetch designed for browsers or Node 18+. Bun’s fetch is not a polyfill; it’s a Zig-implemented native binding. The BunInstrumentation package handles this correctly by hooking into the runtime’s internal dispatch layer rather than wrapping the global function. Always verify your instrumentation packages explicitly list Bun compatibility in their README before installing.

How do you capture distributed traces across Bun microservices?

Tracing within a single Bun process is straightforward. The real test of observability for Bun with OpenTelemetry emerges when requests cross service boundaries. Distributed tracing depends entirely on correct context propagation, and Bun’s native fetch respects W3C Trace Context headers automatically when instrumented.

API GatewayUser ServicePayment ServiceNotification SvcGET /users/123traceparent: 00-abc-def-01POST /chargetraceparent: 00-abc-ghi-01POST /notifytraceparent: 00-abc-jkl-01202 Accepted200 OK200 OK + user payload
Distributed tracing propagates traceparent headers automatically across Bun services when OpenTelemetry is configured correctly.

Validating header propagation

Before deploying to production, verify that trace context flows correctly. A simple test endpoint can confirm headers are injected and extracted:

// test-propagation.ts
Bun.serve({
  port: 3001,
  async fetch(req) {
    // BunInstrumentation auto-extracts traceparent from incoming headers
    const traceId = req.headers.get('traceparent');
    
    // Outbound fetch auto-injects updated traceparent
    const upstream = await fetch('http://payment-service:3002/charge', {
      method: 'POST',
      body: JSON.stringify({ amount: 99.99 }),
    });
    
    return Response.json({ 
      receivedTraceParent: traceId,
      upstreamStatus: upstream.status 
    });
  },
});

If downstream services report disconnected traces (separate trace IDs), check two things: first, ensure BunInstrumentation is registered before any server starts; second, verify no middleware or proxy strips the traceparent header. Nginx reverse proxies, commonly used in front of Bun apps, preserve these headers by default, but custom Lua scripts or Cloudflare Workers may inadvertently remove them.

Manual span creation for business logic

Auto-instrumentation covers I/O boundaries, but critical business decisions inside handlers need explicit spans. Use the OpenTelemetry API directly:

import { trace } from '@opentelemetry/api';

const tracer = trace.getTracer('order-processing');

async function processOrder(orderId: string) {
  return tracer.startActiveSpan('validate-and-reserve-inventory', async (span) => {
    try {
      span.setAttribute('order.id', orderId);
      const result = await reserveInventory(orderId);
      span.setStatus({ code: 1 }); // OK
      return result;
    } catch (err) {
      span.recordException(err);
      span.setStatus({ code: 2, message: err.message });
      throw err;
    } finally {
      span.end();
    }
  });
}

This level of granularity transforms generic HTTP spans into actionable debugging artifacts. When investigating latency, you’ll immediately see whether time was spent in validation, inventory reservation, or serialization rather than guessing.

How do you export Bun telemetry to Grafana Tempo and Prometheus?

Data collection is meaningless without a backend to query it. For teams already running the Prometheus and Grafana stack, integrating Bun telemetry requires configuring dual exporters: OTLP for traces and Prometheus remote write (or scrape) for metrics.

Configuring OTLP trace export to Tempo

Grafana Tempo accepts OTLP over HTTP/gRPC natively. Set the endpoint to your Tempo distributor:

# Environment variables for Bun container
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://tempo-distributor:4318/v1/traces
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OTEL_SERVICE_NAME=bun-checkout-api
OTEL_RESOURCE_ATTRIBUTES=deployment.environment=production,cloud.region=ap-south-1

In Kubernetes deployments, inject these via ConfigMap or Secrets Manager. Avoid hardcoding endpoints in source; use environment-specific overrides so local development can target a collector while production targets the managed backend directly.

Exposing Prometheus metrics from Bun

Bun doesn’t expose a /metrics endpoint by default. Add one using the Prometheus exporter alongside your trace configuration:

// metrics-server.ts
import { PrometheusExporter } from '@opentelemetry/exporter-prometheus';
import { MeterProvider } from '@opentelemetry/sdk-metrics';

const exporter = new PrometheusExporter({ port: 9464 });
const meterProvider = new MeterProvider();
meterProvider.addMetricReader(exporter);

// Register custom metrics
const meter = meterProvider.getMeter('bun-app');
const requestCounter = meter.createCounter('http_requests_total', {
  description: 'Total HTTP requests processed',
});

// Expose in your main server or as separate health/metrics endpoint
Bun.serve({
  port: 9464,
  async fetch(req) {
    if (req.url.endsWith('/metrics')) {
      const { contentType, body } = await exporter.collect();
      return new Response(body, { headers: { 'Content-Type': contentType } });
    }
    return new Response('Not Found', { status: 404 });
  },
});

Configure Prometheus to scrape :9464/metrics. For high-cardinality safety, always bound histogram buckets and avoid unbounded label sets. Review our guidance on defining meaningful SLIs and SLOs to select metrics that actually reflect user experience rather than vanity counters.

Structured logging correlation

Traces without correlated logs force engineers to manually stitch timelines. Configure Bun’s logger (or pino/winston) to inject trace context into every log line:

import { trace } from '@opentelemetry/api';

function logWithTrace(level: string, message: string, attrs?: Record<string, unknown>) {
  const span = trace.getActiveSpan();
  const ctx = span?.spanContext();
  
  console.log(JSON.stringify({
    timestamp: new Date().toISOString(),
    level,
    message,
    trace_id: ctx?.traceId,
    span_id: ctx?.spanId,
    ...attrs,
  }));
}

// Usage in handler
logWithTrace('info', 'Order validated', { orderId: 'ORD-8821', items: 3 });

This produces JSON logs that Loki or Elasticsearch can join against traces using trace_id. Structured logging is non-negotiable for production systems; see our structured logging best practices guide for field naming conventions and retention strategies.

Native --otel Flag✓ HTTP Server Spans✓ Native Fetch Tracing✓ bun:sqlite Auto-Spans✗ Custom Metric Exporters✗ Third-Party DB Drivers✗ Log Correlation Hooks⚠ Limited Sampler OptionsFull SDK + Preload✓ HTTP Server Spans✓ Native Fetch Tracing✓ bun:sqlite Auto-Spans✓ Custom Metric Exporters✓ Third-Party DB Drivers✓ Log Correlation Hooks✓ Full Sampler Control
Choosing between native flag and full SDK depends on whether you need custom metrics, third-party driver support, or advanced sampling for observability for Bun with OpenTelemetry.

Production checklist for reliable Bun observability

Getting instrumentation working locally is step one. Keeping it reliable under load requires deliberate operational choices. In practice, these items separate teams that trust their dashboards from those who disable telemetry after the first performance complaint:

  1. Sample aggressively in dev, selectively in prod. Use always_on during development to catch missing spans. Switch to parentbased_traceidratio with 0.1–0.5 ratio in production to control cost while preserving error traces.
  2. Set resource attributes at deploy time. Inject service.version, deployment.environment, and k8s.pod.name via environment variables. Hardcoded values become stale the moment you deploy a new revision.
  3. Validate exporter connectivity on startup. Add a health check that confirms the OTLP endpoint responds. Silent exporter failures mean zero visibility during the exact incident you need data most.
  4. Monitor telemetry overhead. Measure p99 latency with and without instrumentation. If overhead exceeds 3%, review sampler config or switch to async batch exporters. Bun’s speed advantage shouldn’t be eaten by synchronous telemetry flushes.
  5. Pin instrumentation package versions. Bun’s rapid release cycle occasionally breaks compatibility. Lock versions in bun.lockb and test upgrades in staging before production rollout.

Next steps for your Bun observability stack

Implementing observability for Bun with OpenTelemetry gives you the raw signal pipeline, but signals alone don’t reduce MTTR. Pair this setup with actionable alerting tied to SLOs, dashboard templates that highlight golden signals, and runbooks that reference specific trace attributes. Start with the native --otel flag to validate your backend integration today, then graduate to the full SDK when you need custom metrics or third-party driver coverage. If your team needs help designing a production-grade observability architecture for Bun or auditing your current setup, reach out to discuss your specific requirements.

Frequently Asked Questions

No, Bun lacks built-in OpenTelemetry support. You must use the official Node.js SDK with Bun's Node compatibility layer or community-maintained Bun-specific instrumentation packages for tracing and metrics collection.

Run bun add @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node. Then create a tracing.ts file to initialize the SDK before your main application entry point loads.

Yes, most Node.js auto-instrumentations work via Bun's compatibility layer. Test each library individually as some rely on internal Node APIs that Bun may not fully implement yet.

Typically under five percent CPU overhead with batching enabled. Use sampling and async exporters to minimize latency impact on high-throughput Bun HTTP servers and workers.

Set OTEL_PROPAGATORS environment variable to tracecontext,baggage. The SDK automatically injects and extracts W3C headers from Bun.serve request and response objects.

OTLP gRPC exporters perform best due to lower serialization overhead. Use @opentelemetry/exporter-trace-otlp-grpc with batch span processors for production Bun deployments.

No, each Bun worker requires separate SDK initialization. Pass trace context explicitly via message passing or shared storage to correlate spans across worker boundaries.

Access the active span via trace.getActiveSpan() inside request handlers. Call setAttribute with key-value pairs like user.id or request.path for filtering.

Yes, use @opentelemetry/host-metrics and custom metric instruments. Track Bun-specific values like memory usage, GC pauses, and active connections via periodic callbacks.

Database drivers often lack Bun-compatible instrumentation. Manually wrap query functions with trace.getTracer().startActiveSpan or contribute patches to upstream instrumentation libraries.

Yes, when configured with bounded queues, sampling, and non-blocking exporters. Never enable debug logging or synchronous exports in production Bun environments.

Enable OTEL_LOG_LEVEL=debug temporarily. Verify SDK initializes before app code, check exporter endpoint connectivity, and confirm no unhandled promise rejections drop spans.

Yes, point your OTLP exporter to Datadog's intake endpoint. Map resource attributes to Datadog tags using their OpenTelemetry mapping documentation for proper service identification.

Bun has fewer mature instrumentation libraries but faster execution. Expect gaps in third-party integrations requiring manual instrumentation compared to Node.js ecosystem coverage.

Use parent-based trace ID ratio sampling at one to ten percent. This preserves complete traces for sampled requests while dropping unsampled ones early to reduce export volume.