Benchmarks
This page describes the benchmark harness in the repository, shows how logixia compares with pino and winston on Node 26, and explains the results, including the cases where logixia is slower. The maintainer of logixia wrote the harness. That is a conflict of interest, so the methodology is documented in full and you can run it yourself.
Results
Higher throughput is better. The table shows operations per second for pino and for logixia in JSON mode, and logixia's difference from pino in the same scenario (Node 26):
| Scenario | pino | logixia (JSON) | logixia vs pino |
|---|---|---|---|
| Simple string | 7.5M | 4.2M to 4.4M | -44% to -41% |
| Structured, 5 fields | 1.9M | 2.8M | +49% to +51% |
| Error object | 1.2M | 1.0M to 1.1M | -16% to -13% |
| Child logger | 1.6M | 0.79M to 0.81M | -50% to -49% |
| Deep nested object | 2.4M to 2.5M | 1.7M to 1.9M | -29% to -23% |
| 12-field object | 0.85M to 0.88M | 2.0M to 2.1M | +134% to +145% |
These are two back-to-back runs on Node 26.10 (Apple Silicon, November 2026); a range means the runs differed.
Versions of this page before November 2026 quoted numbers from an older harness that gave pino a slower sink than logixia, which inflated logixia's ratio on every row. The rows above come from the current harness, which gives every library the same kind of sink; a rework of child() and of how the logger stores its config, also in November 2026, is what moved them since.
winston was slower than pino in every scenario except the error object row, where it is given less work (see below).
Read these as ratios from one harness, not as promises about your service. Absolute operations per second depend on the machine, Node version, payload shape and load, and the ratios move between runs. Two cautions on specific rows:
- Child logger. Every library creates the child inside the timed function and logs one line through it, which is what a per-request child costs. logixia is about half pino's speed here: a logixia child is a full logger with its own level, timers and field toggles, while a pino child is little more than a prototype link and a prebuilt string of bindings. A rework of
child()in November 2026 took this row from -82% to about -50%. If you log many lines per request, create the child once per request, not per line. Earlier versions of this page reported +33% for this row from a harness that built the logixia child once, outside the timer; that number was wrong. - Error object. The three libraries are not given identical work: pino receives
{ err }, logixia receives theError, and winston receives onlyerr.message. The row compares behavior on the harness's inputs, not like-for-like serialization of stack traces.
What does the harness do?
The harness is benchmarks/run.mjs. It uses tinybench and runs six scenarios against pino, winston, logixia in its default text mode, logixia in JSON mode, and bunyan when it is installed (bunyan is optional and its rows are skipped if it is not).
How it keeps the comparison as even as it can:
- Same sink, no real I/O. Every library writes to something that drops the line, so the benchmark measures serialization and logger overhead, not disk or terminal speed. pino and bunyan write to an object with a bare
write()that returnstrue. winston uses a synchronous transport that reads the formatted message and calls back. logixia's console output is swallowed by replacingprocess.stdout.writeandprocess.stderr.writewhile timing. - Same call style. Every call is made without awaiting. logixia methods return a promise; awaiting it would add a microtask hop that pino never pays, so the harness calls them the way most application code does.
- JSON mode is the apples-to-apples row. logixia is measured both in its default text mode and in JSON mode (
format: { json: true }); the head-to-head summary compares JSON mode with pino's JSON. - Time per task. Each task runs for 3 seconds by default (
BENCH_TIME_MSoverrides it). The runner reports throughput and p99 latency.
The scenarios:
| Scenario | What is logged |
|---|---|
| Simple string | 'User logged in' |
| Structured log (5 fields) | message plus a 5-field object |
| Error object | an Error plus a message (see the caution above) |
| Child / per-request logger | child() with one binding, then one log line, inside the call |
| Deep nested object | a 3-level object with headers, query and user data |
| High-cardinality metadata | a 12-field object |
What the harness does not measure: real I/O (files, network, a log collector), redaction, sampling, plugins or any logixia transport other than the intercepted console output, multi-process behavior, memory use, or startup time. Turning on redact adds work to logixia's per-line cost, and none of that is in these numbers.
Why does pino win on simple strings?
Pino's design goal is to do as little as possible per call, and a plain string message is where that shows most. logixia does extra work on every line that pino skips by default:
- It reads two
AsyncLocalStoragestores, the request context and the trace-ID scope, to merge request fields into the entry. - It writes a trace ID onto the entry (
traceId: falseremoves this). - It formats an ISO timestamp string, where pino writes an epoch number.
Each is small, but on a call that does almost nothing else, they dominate. That is the price of the features they support: a trace ID on every line without passing a logger around, and timestamps people can read.
As flat payloads get wider, fixed per-line costs matter less and serialization dominates, which is why the structured and 12-field rows go the other way. The error object and deep nested object rows are losses for logixia (about -15% and -26%), and I have not profiled them yet, so I will not guess at the cause.
How do I reproduce the benchmarks?
From a checkout of the repository:
npm ci
npm run build && node benchmarks/run.mjsnpm run benchmark runs the same two steps. The output lists operations per second, p99 latency and the difference from pino for each scenario, then a head-to-head summary of logixia (JSON) against pino. Set BENCH_TIME_MS to shorten the run on a constrained machine. The harness keeps one latency sample per call, so memory grows with run time.
When you run it, close other work, run it more than once and look at the spread. Better still, adapt the scenarios in the file to the payloads your service really logs, and write to the sink you really use.
What should I take from this?
- If your hot path is millions of plain string logs per second, use pino. It is faster, and the harness shows it.
- If your logs are flat structured objects, the gap narrows or reverses in this harness, but your destination, serialization of your own data and I/O will usually matter more than logger overhead.
- Treat any logger benchmark, including this one, as a starting point. See also the best Node.js logging libraries and pino vs winston vs logixia, which publishes an earlier run on Node 22 with a different machine.