# pino-http and the cost of logging every request

> pino-http is the HTTP request logger for the pino logging library, wrapping each request and response in a structured log line with a request id, a response time and the headers. The interesting part of its design is how much of the default output it lets you remove, because the automatic request completed line is the one that costs you the most in volume and the least in value.

**pinojs/pino-http** — 🌲 high-speed HTTP logger for Node.js

- Repository: https://github.com/pinojs/pino-http
- Stars: 708 · Forks: 131
- Language: JavaScript
- License: MIT
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/pinojs-pino-http

## A logger that writes one line per request and one per response

pino-http is the HTTP companion to pino, and the README describes it as a high-speed HTTP logger for Node.js. It wraps the standard http module, and the example is short enough to see the whole shape:

```js
const http = require('http')
const server = http.createServer(handle)

const logger = require('pino-http')()

function handle (req, res) {
  logger(req, res)
  req.log.info('something else')
  res.end('hello world')
}

server.listen(3000)
```

The key line is logger(req, res). Calling it attaches a child logger to the request, and from that point req.log is available for your own messages. The application log line and the request line therefore share a request id, which is what makes the output useful when you are reconstructing a request across several services.

The response side is automatic. When the request finishes, pino-http emits a request completed entry containing the status code, the response headers, the response time in milliseconds and the request details again. If the request errors, you get a request errored entry instead.

The output is newline-delimited JSON, one object per line, which is the format log shippers expect. The README shows it piped into pino-pretty for reading, which is a development convenience rather than a requirement: in production you would leave it as JSON and let the shipping pipeline handle it.

## The benchmark numbers and what they actually measure

The README leads with a claim, that to its knowledge pino-http is the fastest HTTP logger in town, and then gives the numbers.

The benchmarks log each request and response pair while returning a hello world, using autocannon with 100 connections and 10 pipelined requests. The results are: http-ndjson with equivalent info at 7730.73 requests per second, http-ndjson with standard minimum info at 9522.37, pino-http at 21496, pino-http in what it calls an extreme configuration at 25770.91, and no logger at all at 46139.64.

All of these were taken on a Macbook Pro 2013 with a 2.6GHz i7 and 16GB of RAM. That machine is old enough that the absolute numbers are less interesting than the shape: with no logger the server handles roughly twice what it handles with pino-http in its extreme configuration, and roughly half again what it handles with the default pino-http settings.

The two pino-http rows are the ones to compare carefully. The extreme configuration is faster than the default, and the difference between the two is what the automatic logging and the serialisation cost you. Which configuration was measured as extreme is not explained in the benchmark section, so the practical reading is that turning logging down is worth doing on a hot route.

## Twenty ways to change the log line, and what each one costs

The API section is long, and the options fall into groups that are worth understanding rather than reading one by one.

The first group controls the automatic lines. autoLogging set to false disables the automatic request completed and request errored logging entirely, and defaults to true. If you set it to an object you can supply more options, of which autoLogging.ignore takes a function of req and res returning a boolean, which is described as useful for defining logic based on request and response properties such as a user-agent header or a status code, to ignore requests. This is the option that turns off logging for health checks and static assets.

The second group customises the messages. customLogLevel takes a function of req, res and err that returns the level name to log at, and the README notes that a level of silent prevents logging. It is mutually exclusive with useLevel, which sets the level used to log out the response and defaults to info. Then there is a family of message and object hooks: customReceivedMessage, customReceivedObject, customSuccessMessage, customSuccessObject, customErrorMessage and customErrorObject. Each is invoked at a different point, and the object variants replace the base loggable object, with the caller responsible for merging with the loggableObject parameter.

The third group trims what is included. quietReqLogger and quietResLogger, both defaulting to false, change the child logger on req.log and res.log so that instead of the full bindings they carry only the request id at reqId. customAttributeKeys renames the attributes pino-http adds, accepting an object of original to override, with req, res, err, responseTime and reqId available for override. customProps adds properties to every request from a function or object, and wrapSerializers, defaulting to true, controls whether custom serializers receive the raw value.

## Request ids: the default that bites multi-instance deployments

One option deserves its own attention because the README flags the problem directly.

genReqId lets you pass a function that generates the request id, taking req and res as arguments and returning a string or a number. The fallback, the README states, is that pino-http is just using an integer. And then the reason: this default might not be the desired behaviour if you are running multiple instances of the app.

That is a real problem, and it is easy to miss. An integer counter starts at one in each process, so two instances behind a load balancer will both emit request id 1, and a log aggregator will interleave two unrelated requests under the same identifier. Since the request id is the join key between your application logs and the automatic request line, and the join key between services in a distributed trace, a colliding id makes the log actively misleading rather than merely incomplete.

The fix is to pass a genReqId function that returns something unique, and the options are the standard ones: a random identifier, a UUID, or a value derived from the incoming request headers if your upstream already assigns one. The logger option is the other thing to look at, since it is the parent pino instance used for the child logger that pino-http creates.

## Installing it and using it as Express middleware

The install is a single command:

```bash
npm i pino-http --save
```

The package depends on pino, pino-std-serializers, get-caller-file and process-warning, and the version numbers matter for compatibility: pino at 10.0.0 or later, pino-std-serializers at 7.0.0 or later, get-caller-file at 2.0.5 or later and process-warning at 5.0.0 or later.

For Express, the usage is as middleware, and the README shows the shape:

```js
const express = require('express')
const logger = require('pino-http')
```

The difference from the plain http example is that the logger instance is created without arguments and then used as middleware, rather than being called with req and res inside a handler. The README's Express section is cut off after those two lines in the version available here, so the remaining middleware wiring is not shown, but the pattern of creating a logger and passing it to the app is the one the package documents.

The stream is the second parameter of pinoHttp, and it can also be passed as an option under the same name. That is how you direct the output somewhere other than standard output, which matters when the same process is running a server and a worker that should not share a log file.

## What the README does not cover, and where the project stands

There are some gaps worth naming before you commit.

The README does not describe how to log request or response bodies, and given the option list, body logging is not something this package offers. If you need payload samples in your logs, you are writing them yourself in your own handler, and that is a decision about privacy and volume you should make deliberately.

It also does not cover redaction. There is a customProps hook for adding properties, and pino itself has mechanisms for this, but pino-http does not describe a way to strip an authorization header or a cookie from the logged headers. The default output includes the request headers, as the example output shows, so if you log headers you are logging credentials unless you remove them.

On the project side, the repository is not archived and the last push was on 2026-09-14. The tagged releases are v11.0.0 from 2025-10-04, v10.5.0 from 2025-07-30 and v10.4.0 from 2025-01-12. The 11.0.0 major bumps the pino dependency to 10, which tells you the major version tracks the pino major version rather than anything happening inside pino-http itself.

The licence is MIT, the author is David Mark Clements with Matteo Collina as a contributor, and the source layout includes a benchmarks directory, a deprecations.js file, TypeScript type tests in index.test-d.ts and import.test-d.ts, and an eslint config.

## Conclusion

pino-http is a good fit for a Node.js service that already uses pino or wants structured JSON logs without paying for a generic request logger, because the output is machine-readable, the request id ties application logs to the request line, and every automatic field can be trimmed. It is a poor fit for a service that needs human-readable logs in development with no extra tooling, and a poor fit for one that wants to log bodies, since the README describes no body logging option. Before adopting, install it with npm i pino-http --save, decide whether the automatic request completed line is worth its volume on your busiest route, and set genReqId if you run more than one instance of the app, because the default integer counter is per process.

## FAQ

### How do I install pino-http?

Run npm i pino-http --save. The package requires pino at version 10.0.0 or later, pino-std-serializers at 7.0.0 or later, get-caller-file at 2.0.5 or later and process-warning at 5.0.0 or later.

### How do I stop pino-http from logging every request automatically?

Set autoLogging to false to disable the automatic request completed and request errored messages, or use autoLogging.ignore with a function of req and res returning a boolean, which the README suggests for ignoring requests based on properties such as user-agent header or status code.

### Why should I set genReqId in pino-http?

The fallback request id is an integer, and the README notes that this default might not be the desired behaviour if you are running multiple instances of the app. An integer counter restarts in each process, so instances behind a load balancer can produce colliding request ids.

### Can I use pino-http with Express?

Yes, the README documents using it as Express middleware by creating a pino-http logger instance and passing it to the app. The stream can also be passed as the second argument of pinoHttp or as the stream option, which is how you direct output somewhere other than standard output.

## Sources

- [Issues](https://github.com/pinojs/pino-http/issues)
- [License: MIT](https://github.com/pinojs/pino-http/blob/master/LICENSE)
- [pinojs/pino-http on GitHub](https://github.com/pinojs/pino-http)
- [README](https://github.com/pinojs/pino-http/blob/master/README.md)
- [Releases](https://github.com/pinojs/pino-http/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-http
