
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
Bun’s exceptional raw performance means that naive logging implementations can quickly become your primary bottleneck in high-throughput environments. Effective production logging for Bun applications demands an asynchronous, non-blocking architecture that serializes logs outside the main event loop while maintaining strict compatibility with existing observability stacks. This guide covers the exact configuration patterns I use to instrument Bun services safely, ensuring you capture actionable telemetry without sacrificing the runtime's speed advantages or violating compliance requirements.
How do you configure structured production logging for Bun applications?
The foundation of reliable structured logging best practices applies directly to Bun: emit machine-parseable JSON, not human-readable text. While Bun includes a basic console implementation, it lacks the performance characteristics required for production workloads handling thousands of requests per second. The industry-standard approach is integrating pino, which supports Bun natively and provides the low-overhead, asynchronous logging primitives necessary to avoid blocking your application.
Installing and initializing pino for Bun
Pino works with Bun without modification because Bun implements the Node.js API surface. Install the core package and the async transport worker:
bun add pino pino-pretty Create a dedicated logger module that enforces consistent configuration across your application. Never instantiate loggers inline; centralize creation to guarantee uniform field naming, redaction rules, and transport settings:
// src/logger.ts
import pino from 'pino';
export const logger = pino({
level: process.env.LOG_LEVEL || 'info',
base: { service: 'bun-api', env: process.env.NODE_ENV },
timestamp: pino.stdTimeFunctions.isoTime,
redact: {
paths: ['req.headers.authorization', 'req.headers.cookie', 'password', 'token', 'secret'],
censor: '[REDACTED]',
remove: false
},
transport: process.env.NODE_ENV === 'production'
? { target: 'pino/file', options: { destination: 1 } }
: undefined
}); This configuration outputs ISO-8601 timestamps (essential for cross-service correlation), injects baseline metadata for filtering, and critically, redacts sensitive headers and fields before serialization. In production, it writes directly to file descriptor 1 (stdout) using pino’s built-in synchronous file writer, which is safe because container runtimes and orchestrators buffer stdout efficiently. For local development, omitting the transport enables pretty-printing via pino-pretty as a separate CLI pipe, keeping production dependencies minimal.
Enforcing NDJSON output format
Newline-delimited JSON is non-negotiable for production. Each log line must be a complete, self-contained JSON object terminated by \n. This format allows streaming parsers like Fluent Bit, Vector, or Logstash to process logs incrementally without buffering entire files. Pino defaults to NDJSON when no pretty-printer is active. Verify your output locally:
echo '{"msg":"test"}\n{"msg":"test2"}' | jq -c . If parsing fails on any line, your aggregator will drop records silently. Always validate log shape in CI using schema checks against your expected log contract.
Why must Bun logging be asynchronous and non-blocking?
Bun’s performance advantage comes from its single-threaded, event-loop-driven architecture. Synchronous I/O operations—including file writes—block the entire event loop, preventing request processing, timer execution, and promise resolution. At 10,000 RPS, even 50µs of synchronous logging overhead per request translates to 500ms of cumulative blocking per second, destroying latency percentiles and throughput.
Pino solves this through its transport system, which spawns a separate worker thread (or uses Bun’s native worker support) to handle serialization and I/O. The main thread only performs an in-memory enqueue operation (~1–5µs), returning control immediately. The worker batches writes, applies redaction, and flushes to the destination independently. This decoupling is what makes production logging for Bun applications viable at scale.
Configuring async transport correctly
Always specify transport targets explicitly. Avoid passing functions or complex objects as transport options—they cannot be serialized across thread boundaries. Use string targets resolved at runtime:
transport: {
targets: [
{
target: 'pino/file',
options: { destination: 1, mkdir: true },
level: 'info'
},
{
target: './transports/audit.ts',
options: { path: '/var/log/audit.ndjson' },
level: 'warn'
}
]
} Custom transports must export a default function returning a writable stream. Test them in isolation before deploying; a misbehaving transport can silently drop logs or crash the worker without affecting the main process, creating dangerous observability gaps.
How do you integrate OpenTelemetry tracing with Bun logs?
Logs without trace context are nearly useless in distributed systems. You need to inject trace_id and span_id into every log entry so operators can jump from a log line to the full request trace in Jaeger or Tempo. I cover the broader instrumentation strategy in instrumenting apps with OpenTelemetry, but Bun-specific integration requires explicit child logger creation per request.
Middleware pattern for trace correlation
Create middleware that extracts the active span and derives a child logger bound to that trace context:
// src/middleware/logging.ts
import { trace } from '@opentelemetry/api';
import { logger } from '../logger';
export function loggingMiddleware() {
return async (ctx, next) => {
const span = trace.getActiveSpan();
const traceId = span?.spanContext().traceId;
const spanId = span?.spanContext().spanId;
const reqLogger = traceId
? logger.child({ trace_id: traceId, span_id: spanId })
: logger;
ctx.set('logger', reqLogger);
const start = Date.now();
try {
await next();
reqLogger.info({
msg: 'request completed',
method: ctx.req.method,
path: ctx.req.path,
status: ctx.res.status,
duration_ms: Date.now() - start
});
} catch (err) {
reqLogger.error({
err,
msg: 'request failed',
method: ctx.req.method,
path: ctx.req.path,
duration_ms: Date.now() - start
});
throw err;
}
};
} This pattern guarantees every log within a request handler carries trace identifiers. Use ctx.get('logger') throughout handlers instead of importing the global logger directly. The child logger inherits all parent configuration (redaction, base fields) while adding trace context immutably.
What are the common pitfalls in Bun production logging?
Even experienced teams make mistakes when adapting Node.js logging patterns to Bun. These are the failures I see repeatedly in audits and incident reviews:
- Using console.log in production: Bun’s console is synchronous and unstructured. It bypasses redaction, lacks levels, and blocks the event loop. Reserve it exclusively for startup messages before the logger initializes.
- Logging full request/response bodies: This creates PII exposure risks, inflates storage costs, and slows serialization. Log only identifiers, statuses, and durations. Reference protecting PII in applications for redaction strategies applicable beyond AI contexts.
- String interpolation in log calls:
logger.info(`User ${id} logged in`)defeats structured parsing. Always pass objects:logger.info({ user_id: id, msg: 'user logged in' }). - Ignoring log backpressure: If your transport cannot keep up with emission rate, pino’s internal buffer grows unbounded. Monitor
pino.transport.droppedmetrics and setsync: falsewith bounded buffers in high-volume scenarios. - Missing log rotation: Writing to stdout in containers is fine, but writing to files on VMs requires external rotation. Use
logrotateor delegate to a sidecar; never implement rotation in-application.
| Approach | Event Loop Impact | Structured Output | Trace Correlation | Production Viable |
|---|---|---|---|---|
| console.log | Blocking (sync) | No | Manual | No |
| Bun.file().write | Blocking (sync path) | Manual JSON | Manual | Risky |
| pino (sync) | Blocking | Yes | Child logger | Low volume only |
| pino + async transport | Non-blocking | Yes (NDJSON) | Child logger | Yes |
| Custom worker logger | Non-blocking | Depends | Manual | High maintenance |
How do you validate and monitor Bun logging pipelines?
Configuration alone is insufficient. You must verify logs are well-formed, reaching their destination, and not being dropped. Add these validation steps to your deployment pipeline:
- Schema validation in CI: Capture sample log output during integration tests. Validate against a JSON Schema defining required fields (
level,time,msg,service) and forbidden fields (password,authorization). Fail the build on violations. - Transport health checks: Expose a
/health/logsendpoint that verifies the logger’s transport worker is alive and buffer depth is below threshold. Return 503 if backpressure exceeds safe limits; this signals upstream load balancers to stop sending traffic before logs are lost. - Drop rate monitoring: Instrument pino’s internal counters. Alert when
droppedincrements exceed 0.1% of emitted logs over 5 minutes. This indicates transport saturation requiring scaling or optimization. - End-to-end trace verification: In staging, trigger a test request and verify the corresponding log appears in your centralized system with correct trace_id within 30 seconds. Automate this as a synthetic check.
For teams managing databases alongside Bun services, ensure log retention policies align with your data lifecycle. My guides on PostgreSQL administration essentials and MongoDB administration basics cover complementary retention strategies for audit logs stored in-database versus external aggregators.
Implementing Production Logging for Bun Applications Safely
Effective production logging for Bun applications balances three constraints: performance (non-blocking I/O), compliance (redaction + retention), and observability (structure + correlation). Start with pino’s async transport, enforce NDJSON output, bind trace context via child loggers, and validate your pipeline end-to-end before handling real traffic. Treat logging configuration as infrastructure code: version it, test it, and monitor it with the same rigor as your application logic. If your team needs help auditing or implementing observability for Bun services, reach out to discuss your specific requirements.