11. Logs
1. The rule: events to stdout, and nothing more
The factor is one sentence: treat logs as event streams. Each running process writes its stream of events, unbuffered, to stdout, and that is the whole of the app’s responsibility. It doesn’t open a logfile, it doesn’t rotate one, and it doesn’t decide where its output is stored or how long it’s kept. It emits events to its own output stream and leaves everything downstream of that to someone else.
That someone else is the point of the factor, and the hand-off is deliberate: the app produces, the environment decides. As the series introduction put it, a twelve-factor app treats its logs as an unbuffered stdout stream collected by the platform. Unbuffered is doing real work in that sentence: you want each event to leave the process the moment it happens, not sit in a buffer a crash could erase before it flushes. Write it and let it go.
2. A log is a stream, not a file
A log is a stream of time-ordered events, one event per line as a rule, flowing continuously with no fixed beginning and no end for as long as the process runs. A multi-line exception backtrace is the usual break from one-line-per-event. A file full of log lines on disk is one possible output format for that stream, and a familiar one, but it isn’t what a log fundamentally is. Fix that distinction and the rest of the factor follows from it.
The reframe isn’t philosophy; the process model forces it. A twelve-factor process is stateless, per Processes, and disposable, per Disposability, so it owns no stable patch of disk for long. It gets killed on a deploy, restarted on a config change and relocated onto another machine whenever the platform wants to pack its nodes better. A logfile the process wrote locally goes with it: erased on the machine it left, or scattered in fragments across every machine it ever ran on, with no single file holding the whole story. Writing to stdout and letting the environment collect the stream is the only approach that survives a process you’re allowed to kill at any moment.
3. The app emits; the environment routes
The app emits its event stream to stdout; the environment it runs in captures that stream and decides where it goes. The app never names a destination. That division of labor is the same one config uses.
In local development that environment is your terminal. The developer runs the process in the foreground and watches the stream scroll past, which is all the log handling a laptop needs. In staging and production the execution environment does the heavy lifting: it captures the stdout stream from every running process, collates all of those streams into one, and routes the result to one or more destinations for viewing now and archival later. Those destinations aren’t visible to the app and aren’t configurable from inside it; an open-source router like Fluentd sits in the environment and owns that decision. It’s the shape Config establishes for configuration, applied to output: the app doesn’t choose where its logs go any more than it chooses which database URL it’s handed. It reads from the environment and writes to the environment, and stays out of the routing business.
4. Indexing, trends, and alerts on the stream
Once logs are a stream, you can point them at a system that does real work on them. The stream flows into a log indexing and analysis system, historically something like Splunk or the ELK stack (Elasticsearch, Logstash, Kibana), now just as often a cloud provider’s logging backend. Getting every event into one queryable place is what turns a wall of scrolling text into something you can actually use.
Three capabilities come out of it. You can find a specific past event: search across every process’s output for the one request that failed, instead of grepping files on whichever machine happened to handle it. You can graph trends at scale: requests per minute, error rates across the day, latency drawn from the same event stream. And you can alert on heuristics: wire a rule that pages someone when errors per minute cross a threshold so a problem announces itself instead of waiting to be noticed. The stream is the raw material observability is built from, and none of it is reachable while your logs are still scattered across local files.
5. The model that won: containers and Kubernetes
This is exactly how a container logs. A container writes its events to stdout and stderr; the container runtime captures both streams; kubectl logs reads that captured stream straight back to you. The application inside the container did nothing but write to standard output, and the platform took care of the rest, which is the factor almost word for word.
Collation is a real piece of infrastructure at cluster scale. A node-level log agent, Fluentd or its lighter-weight sibling Fluent Bit, or Vector, runs on each node, collects the captured stdout streams of every container on that node, and ships them to a backend for indexing and long-term storage: Elasticsearch or Loki if you self-host, CloudWatch or Datadog if you don’t, Splunk either way. It’s the capture-collate-route job from the last two sections, built into how Kubernetes clusters run by default. “Write to stdout, let the platform route and store” stopped being a recommendation and became the shape of the ecosystem.
6. Where the model grew up: structured logs and observability
The factor was right, and the industry built richer things on top of it without disturbing the foundation. Two refinements are worth naming, and both leave the stdout hand-off exactly as it was.
The first is format. 2011’s “one plain-text event per line” has largely become structured logging: each event is a line of JSON on stdout, with named fields a machine can parse, index, and query directly instead of matching patterns against freeform strings. A structured log line is still an event, still written unbuffered to standard output, still captured and routed by the environment. (Keep the fields generic when you emit them; a log line is a poor place for a token or a password, structured or not.) The stream is the same stream; it just got easier for the other end to read.
The second is scope. Logs are now usually described as one of the three pillars of observability, alongside metrics (think Prometheus) and distributed traces (think OpenTelemetry, which increasingly ties all three together). That framing is a useful mental model as long as you read it the right way. Metrics and traces are additional signals layered beside logs, not replacements for them, and each keeps the same separation of concerns this factor prescribed: the app produces the signal, and something external owns storage and querying. The transport differs (Prometheus scrapes a /metrics endpoint the app exposes rather than capturing an emitted stream), but the division of labor is the same. The one genuinely new problem is volume; at scale, log streams get expensive enough that sampling and tiered retention become things you manage on purpose rather than keeping everything forever.
The durable win is the decoupling itself. An app that emits events to stdout and stays out of routing and storage is an app whose logs survive being killed, restarted, containerized, and scaled to a thousand copies, and that same property is what lets the whole observability stack layered above those logs work at all. Keep the app writing to standard output and out of the storage business, and everything downstream, from kubectl logs on a laptop to a paging rule watching a fleet, has something clean to build on.