✦ For everyone, free.

Practical knowledge for real and everyday life

Home

15.1.3.4 App Log Buffering

A focused guide to App Log Buffering, connecting core concepts with practical Docker and container operations.

App log buffering refers to how an application's own runtime holds output in memory before actually writing it out to stdout or stderr, a behavior that exists independently of anything Docker does, and one that frequently causes confusing symptoms, delayed, bursty, or apparently missing log output, that look like a Docker or logging-driver problem but actually originate entirely inside the application's own language runtime.

Why buffering happens at all

Most language runtimes buffer standard output for performance reasons: writing to a file descriptor is a relatively expensive system call, and batching many small writes into fewer, larger ones reduces that overhead significantly. The specific buffering behavior often differs depending on whether the runtime believes its output is connected to an interactive terminal or not:

python3 -c "import sys; print('line', file=sys.stdout)"
python3 -c "import sys; print('line', file=sys.stdout)" | cat

Many runtimes use line buffering (flushing after every newline) when connected to a terminal, but switch to full buffering (flushing only when the buffer fills or the process exits) when output is redirected to a pipe or file, which is exactly the situation a containerized process runs in by default, since its stdout is captured by Docker rather than connected to an interactive terminal.

How this manifests inside a container

A containerized application using full buffering can appear to produce no log output for an extended period, only to suddenly emit a large burst once its internal buffer fills or the process exits, which is easy to mistake for a logging pipeline failure rather than ordinary, expected runtime buffering behavior:

docker logs -f my-api
# (long silence)
# (sudden burst of many lines at once)

This delay has real operational consequences: a health check or monitoring system expecting to see recent log activity as a liveness signal may report a false problem, and an operator investigating an incident in real time may see no new log output even though the application is actively running and logging internally, just not yet flushing.

Disabling buffering at the runtime level

Most languages provide an explicit way to disable or reduce output buffering, which is generally the correct fix for a containerized application, since the performance benefit of buffering matters much less than the observability cost in most production scenarios:

ENV PYTHONUNBUFFERED=1
node --no-warnings server.js # Node.js stdout to a pipe is already typically unbuffered by default
System.setOut(new PrintStream(new FileOutputStream(FileDescriptor.out), true));

Python is one of the more commonly encountered cases specifically because its default behavior switches noticeably between interactive and piped output; many other runtimes, including Node.js for typical console output, are unbuffered or line-buffered by default even when piped, which is part of why this issue is more associated with certain languages than others.

Explicit flushing in application code

Where a runtime does not offer a global unbuffered mode, or where buffering happens at the logging library level rather than the language runtime level, explicitly flushing after critical log statements ensures they are not held in a buffer indefinitely:

import sys
print("Critical event occurred", flush=True)
logger.info('Critical event occurred');
process.stdout.write(''); // some logging libraries expose an explicit flush method instead

For most logging libraries, an explicit flush is more idiomatically exposed through the library's own API rather than through manipulating the underlying stream directly, and consulting the specific logging library's documentation for its flush behavior is more reliable than assuming a generic stream-flushing approach will interact correctly with however that library manages its own internal buffering.

Buffering inside logging libraries themselves

Beyond language runtime buffering, many logging libraries implement their own internal buffering or asynchronous writing for performance, which is a separate layer that needs to be configured independently of the underlying language runtime's stdout behavior:

const logger = require('pino')({}, pino.destination({ sync: false }));
const logger = require('pino')({}, pino.destination({ sync: true }));

Asynchronous logging libraries trade a small amount of write latency reduction for a window during which log lines exist only in memory and would be lost if the process crashed or was killed before that buffer flushed, which is a meaningful consideration for log lines produced immediately before a crash, exactly the lines most valuable for understanding what went wrong.

The interaction with graceful shutdown

Buffered log output that has not yet flushed at the moment a process exits, whether through a normal shutdown or a crash, is lost, which means a graceful shutdown handler should explicitly flush logging buffers before the process actually exits, in addition to closing other resources:

process.on('SIGTERM', async () => {
  await logger.flush();
  server.close(() => process.exit(0));
});

This is a small but easily overlooked detail: a shutdown sequence that closes the server and database connections cleanly but never explicitly flushes a buffered logger can still lose the final, often most diagnostically relevant, log lines produced during the shutdown sequence itself.

Diagnosing whether buffering is the cause of a symptom

When log output appears delayed or missing in bursts, a quick diagnostic is forcing a large amount of output and observing whether it appears immediately or only after a delay or in a sudden burst, which distinguishes a buffering issue from a genuine pipeline failure:

docker exec my-api sh -c 'for i in $(seq 1 1000); do echo "test $i"; done'
docker logs --tail 5 my-api

If this burst of output appears with a noticeable delay relative to when it was actually written, buffering inside the application or its runtime is a strong candidate explanation, separate from anything related to Docker's own log capture or driver configuration.

Common mistakes

  • Diagnosing delayed or bursty log output as a Docker or logging-driver problem without first checking whether application-level buffering is the actual cause.
  • Leaving a language runtime's default piped-output buffering behavior in place in production, where the observability cost outweighs the marginal performance benefit.
  • Using an asynchronous logging library without explicitly flushing it during graceful shutdown, losing the most recent and often most diagnostically relevant log lines on every clean exit.
  • Assuming all language runtimes buffer output identically, when buffering defaults vary significantly between languages and even between different output destinations within the same language.
  • Not testing whether a fix for buffering actually changed observed behavior, relying instead on documentation describing expected behavior without confirming it empirically inside the actual container.

App log buffering is an application and language-runtime concern that frequently produces symptoms indistinguishable, at first glance, from a Docker logging pipeline problem, and resolving it usually means disabling or explicitly managing buffering at the runtime or logging library level rather than adjusting anything in Docker's own logging configuration.