# pino: a JSON logger for Node.js that writes from a worker thread

> pino is a low-overhead JSON logger for Node.js, with child loggers for request context and a transport API that moves log processing off the event loop. It suits services that ship structured logs to a collector, not teams that want a human-readable console by default.

**pinojs/pino** — 🌲 super fast, all natural json logger

- Repository: https://github.com/pinojs/pino
- Website: http://getpino.io
- Stars: 18,233 · Forks: 1,003
- Language: JavaScript
- License: MIT
- Published: 2026-09-21 · Updated: 2026-09-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/pinojs-pino

## The problem pino solves: logging that does not tax the event loop

Node.js runs application code on a single thread. Anything the logger does synchronously, from formatting a message to serializing an object to writing to a socket, competes with request handling. The README makes this the project's central claim: "Log messages tend to get added over time and this can lead to a throttling effect on applications, such as reduced requests per second." pino's answer is to do as little as possible on the calling thread and to emit JSON rather than formatted text.

The audience is therefore narrow and specific. If your logs go to Elasticsearch, Loki, CloudWatch or any other system that parses structured records, pino hands it one JSON object per line with no parsing step. If your logs are read by a person on a terminal, pino is the wrong default, and the README says so indirectly by pointing at pino-pretty for development only. The project also runs on Bare and on Pear through the pino-bare compatibility module, which matters if you are building on that runtime rather than on Node.js proper.

## How pino builds a log line: base fields, child bindings, serializers

Calling the exported function returns a logger. Every record it writes carries a small set of base fields, and the README's own output shows exactly which ones:

```
{"level":30,"time":1531171074631,"msg":"hello world","pid":657,"hostname":"Davids-MBP-3.fritz.box"}
```

Level is numeric, time is epoch milliseconds, and pid and hostname are added automatically. That last detail is a design decision worth noticing: the default output is not minimal, it is pre-populated with the fields a log aggregator needs to attribute a line to a process on a host.

The child logger is the mechanism that carries request context. A child is created from a parent with a bindings object, and those bindings are merged into every record the child writes. In the README example the child adds `a: 'property'`, and the resulting line ends with that key. In practice this is how a request id, a user id or a route name travels through a call stack without being threaded through every function argument. The repository also ships docs/child-loggers.md and docs/serializers.md, so serializers are a documented extension point for controlling how specific object shapes (errors, requests) get turned into fields.

The repository layout backs this up: pino.js is the entry point, lib/ holds the implementation, pino.d.ts carries the TypeScript types, and browser.js is a separate browser build. The package.json declares `"type": "commonjs"` with `"browser": "./browser.js"`, so bundlers pick the browser entry automatically when you target the browser.

## Installing pino and logging a first request-scoped line

Installation is a single npm or yarn command. The README gives both forms:

```bash
npm install pino
```

```bash
yarn add pino
```

After that, the smallest working program is four lines. This is the README's usage example verbatim in structure: create a logger, log a message, create a child with a binding, log from the child.

```js
const logger = require('pino')()

logger.info('hello world')

const child = logger.child({ a: 'property' })
child.info('hello child!')
```

Run it with `node` and you should see two lines on stdout, each a single JSON object. The first has level 30, a time, the message, pid and hostname. The second adds `"a":"property"` at the end. Nothing is pretty-printed, and that is the expected result, not a misconfiguration.

For local development the README points at pino-pretty, which is a separate module rather than part of pino itself. The repository also includes examples/basic.js and examples/transport.js, which are the two files to read if you want a runnable starting point rather than the README snippet. If you use a web framework, docs/web.md has per-framework sections for Fastify, Express, Hapi, Restify, Koa, Nest, Hono and Node core http; that document, not the README, is where the framework wiring lives.

## Transports: why log processing belongs in another thread

The README's strongest architectural statement is that log processing should not run on the main thread. It recommends that sending, alert triggering and reformatting all happen in a separate process or thread, and names those processors "transports", run through the `pino.transport` API. The reasoning is the same as the low-overhead argument: a transport that formats or ships logs on the main thread reintroduces exactly the cost pino was built to avoid.

This is the part of pino that has the sharpest edges. Moving work into a worker thread means the failure modes move with it. The README does not document what happens when a transport's worker thread dies, whether buffered records are lost, or how to roll back to plain stdout. docs/transports.md is the document that covers transports in detail, and it is the file to read before you commit to a transport in production. Treating the transport API as a drop-in replacement for a synchronous write is a mistake; it is a different execution model with different failure characteristics.

The examples/transport.js file exists precisely because this is not obvious from the README snippet. If you only read the README, you will know that transports exist and that they run in a worker thread. You will not know how to configure one.

## Where pino is the wrong choice

The clearest case against pino is a service whose logs are read by humans on stdout. The default output is JSON with numeric levels and epoch timestamps. Reading that in a terminal is unpleasant, and the README's answer is pino-pretty, which it frames as a development-time tool. Running a pretty-printer in production means every log line goes through a formatter, which is the cost pino was designed to remove.

A second boundary is runtime. pino is built for Node.js. Bare and Pear are supported through pino-bare, but that is a compatibility module maintained separately, not something the core package handles on its own. If your service is not on one of those runtimes, the package is not aimed at you.

A third limitation is structural rather than technical: pino emits structured records and expects something downstream to consume them. If you have no log collector, no query layer and no retention policy, you have traded readable text for JSON that nobody parses. The logger is not the missing piece in that setup.

## pino compared with a conventional formatted logger

The obvious alternative is a logger that formats human-readable lines by default, such as the long-standing winston or bunyan style of logger. The difference is not cosmetic. A formatting logger builds a string, often with color and padding, on the calling thread, and that string then has to be parsed back into fields if a collector wants to query it. pino inverts this: it writes the structured object directly and pushes formatting to a separate tool that runs only when a human is reading.

That inversion has a cost the README does not dwell on. You now maintain two configurations, one for development and one for production, and the two produce different output. A bug that only appears when the pretty-printer is active will not reproduce in production, and vice versa. Teams that value a single output format everywhere will find pino's split awkward, and that is a legitimate reason to choose a formatting logger instead.

The README also states that "In many cases, Pino is over 5x faster than alternatives" and links to docs/benchmarks.md for the comparisons. The repository ships a benchmarks/ directory and a set of npm scripts (bench-basic, bench-object, bench-child, bench-formatters and others) for running them. Whether that factor holds for your workload depends on what you log, and the benchmark scripts are the way to check rather than taking the headline number at face value.

## Maintenance, licence and the cost of upgrading

pino is MIT licensed, which permits commercial use, modification and redistribution provided the copyright notice and licence text are retained. That is the whole of the licence implication; anything beyond it is a question for your own legal review, not something the README or LICENSE file settles.

The repository is not archived, and the last push was on 2026-09-05. Recent releases are v10.3.1 on 2026-02-09, v10.3.0 on 2026-01-23 and v10.2.1 on 2026-01-19, so the 10.x line is where current work sits. The README notes that pino v6 lives on a separate branch, which tells you the project has shipped several major versions and expects users to move between them.

The upgrade cost is not in the core logger, which is small and stable in shape. It is in the surrounding pieces. pino-pretty, pino-bare and any transport you use are separate packages with their own version lines, and the README treats them as separate modules rather than bundled parts. The repository also has a documented long term support policy at docs/lts.md, which is the file to read before deciding how far behind you can afford to fall. The package.json test script runs lint, a transpile step, the borp test runner with 95 percent coverage thresholds, jest, and a type test pass, so the project holds itself to a fairly strict bar; that is a statement about the test setup, not a guarantee about any particular release.

## Conclusion

Adopt pino if your Node.js service already ships logs to a collector that parses JSON, and you are willing to add pino-pretty for local development and a transport for anything beyond stdout. Do not adopt it if you need human-readable output on stdout in production, or if your runtime is not Node.js, Bare or Pear. Before committing, verify two things: that your log pipeline parses one JSON object per line, and that the transport you pick is configured the way docs/transports.md describes, since the README itself does not document rollback or failure handling for transports.

## FAQ

### What does pino do?

pino is a JavaScript logger that writes one JSON object per line, with level, time, message, pid and hostname as base fields. The README describes it as a very low overhead logger and recommends running log processing in a separate thread through the pino.transport API.

### What is the best logger library for Express?

The README does not rank logger libraries, so it cannot answer this. It does document pino with Express in docs/web.md, and the repository also lists Fastify, Hapi, Restify, Koa, Nest, Hono and Node core http in the same document.

### What are the log levels in Pino?

The README shows a level of 30 for an info call, which means levels are emitted as numbers rather than strings. The full level list is documented in docs/api.md, not in the README.

## Sources

- [License: MIT](https://github.com/pinojs/pino/blob/main/LICENSE)
- [pinojs/pino on GitHub](https://github.com/pinojs/pino)
- [Project website](http://getpino.io)
- [README](https://github.com/pinojs/pino/blob/main/README.md)
- [Releases](https://github.com/pinojs/pino/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/pinojs-pino
