
Table of Contents
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.
--otel flag or use the @opentelemetry/sdk-node package with Bun-specific auto-instrumentations. Configure an OTLP exporter to send traces and metrics to your backend, ensuring you instrument both HTTP requests and database calls for complete visibility into application performance.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 dashboardsOTEL_TRACES_SAMPLER: Sampling strategy (always_on,traceidratio, orparentbased_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.
| Aspect | Node.js | Bun |
|---|---|---|
| HTTP Server Instrumentation | Patches http.createServer via require hooks | Native hook via BunInstrumentation or --otel flag |
| Fetch API | Requires third-party polyfill instrumentation | Built-in global fetch is auto-instrumented natively |
| Database Drivers | Separate instrumentation per driver (pg, mysql2) | bun:sqlite native; postgres/mysql via compatible adapters |
| Module Loading | CommonJS/ESM loader hooks | Custom bundler/resolver; preload scripts preferred |
| Context Propagation | AsyncLocalStorage (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.
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.
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:
- Sample aggressively in dev, selectively in prod. Use
always_onduring development to catch missing spans. Switch toparentbased_traceidratiowith 0.1–0.5 ratio in production to control cost while preserving error traces. - Set resource attributes at deploy time. Inject
service.version,deployment.environment, andk8s.pod.namevia environment variables. Hardcoded values become stale the moment you deploy a new revision. - 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.
- 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.
- Pin instrumentation package versions. Bun’s rapid release cycle occasionally breaks compatibility. Lock versions in
bun.lockband 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.