Production Logging for Bun Applications

Khimananda Oli 9 min read Programming and Languages
Production Logging for Bun Applications

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.

Bun AppMain Threadpino logger.info()Worker ThreadAsync TransportSerialize + RedactStdout / FileNDJSON StreamBuffered WritesLog AggregatorFluent Bit / VectorCentralized Store
Non-blocking production logging architecture for Bun applications using worker-based async transport

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.

Synchronous Logging (Blocking)Event LoopRequestLOG WRITERequestBlockedEvent loop halted during disk I/OAsync Logging (Non-Blocking)Event LoopRequestEnqueueRequestRequestWorker ThreadZero event loop interruption
Synchronous logging blocks Bun's event loop; async transport offloads I/O to worker thread

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.dropped metrics and set sync: false with 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 logrotate or delegate to a sidecar; never implement rotation in-application.
ApproachEvent Loop ImpactStructured OutputTrace CorrelationProduction Viable
console.logBlocking (sync)NoManualNo
Bun.file().writeBlocking (sync path)Manual JSONManualRisky
pino (sync)BlockingYesChild loggerLow volume only
pino + async transportNon-blockingYes (NDJSON)Child loggerYes
Custom worker loggerNon-blockingDependsManualHigh maintenance
Start: Need Logging?> 100 RPS or Compliance?NoYespino sync OKNeed Trace Context?pino + async + OTelNopino + async transportNever in Productionconsole.log / Bun.file.writeUnstructured + Blocking
Decision tree for choosing appropriate production logging for Bun applications by workload profile

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:

  1. 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.
  2. Transport health checks: Expose a /health/logs endpoint 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.
  3. Drop rate monitoring: Instrument pino’s internal counters. Alert when dropped increments exceed 0.1% of emitted logs over 5 minutes. This indicates transport saturation requiring scaling or optimization.
  4. 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.

Frequently Asked Questions

Use the built-in console.log with a custom formatter or libraries like pino. Configure Bun to output newline-delimited JSON so log aggregators like Datadog or Loki can parse fields automatically without regex extraction in your 2026 infrastructure stack.

No. Bun lacks built-in file rotation. Use logrotate on Linux or pipe stdout to a sidecar container running fluent-bit to handle size-based rotation and retention policies reliably in Kubernetes or systemd-managed production environments.

Synchronous writes block the event loop and degrade throughput significantly. Always use async logging or buffered streams to prevent I/O bottlenecks during high-concurrency request handling in production Bun applications serving thousands of requests per second.

Yes. Pino works natively with Bun and offers low-overhead async logging. Install via bun add pino and configure transport workers to offload serialization from the main thread for optimal performance under heavy load.

Never log raw PII or secrets. Use pino-redact or custom serializers to mask tokens, emails, and passwords before writing. Validate redaction rules in staging to ensure compliance with GDPR and SOC2 requirements in 2026.

Set info as the baseline and warn for anomalies. Avoid debug in production unless temporarily enabled via environment variables. Excessive verbosity increases storage costs and obscures critical signals during incident response and debugging sessions.

Inject a unique trace ID into every request context using middleware. Propagate this ID through all log entries and downstream calls. OpenTelemetry SDKs for Bun automate this correlation for distributed tracing in 2026 cloud-native architectures.

Yes. Containers are ephemeral and stdout integrates directly with orchestrator log drivers. File logging inside containers risks data loss during restarts and complicates centralized collection compared to standard container logging patterns used in modern deployments.

Use bun test with --bail and measure latency percentiles with and without logging enabled. Compare against baseline HTTP benchmarks using wrk or autocannon to quantify real-world impact on p99 response times under sustained production-like traffic loads.

Not natively. Stream logs to an intermediary like fluentd or vector that buffers and batches uploads. Direct S3 writes from application code introduce latency spikes and failure modes unsuitable for reliable production logging pipelines.

Register process.on('uncaughtException') and process.on('unhandledRejection') handlers that log structured error objects before graceful shutdown. Include stack traces and request context to enable post-mortem analysis without losing diagnostic information during crashes.

Retain hot logs for seven days and archive cold logs for ninety days minimum. Adjust based on compliance needs and storage budgets. Automate lifecycle policies in your log backend to balance cost against forensic investigation requirements.

Partially. Use the official @opentelemetry/sdk-node package with Bun-compatible instrumentation. Manual setup is required for log correlation and export to backends like Jaeger or Grafana Tempo in 2026 observability stacks.

Exclude /health and /ready endpoints at the middleware level or via log aggregation filters. Logging every probe pollutes signal-to-noise ratio and inflates ingestion costs without providing actionable operational value for production monitoring dashboards.

Yes. Enable --sourcemap during build to map minified stack traces back to original TypeScript sources. Without them, error logs reference generated line numbers that hinder rapid debugging and increase mean time to resolution during incidents.