Performance
The cost of logging depends on the fields you collect, serialization, redaction, output, and delivery. The repository includes microbenchmarks for those operations, but an event-construction measurement is not an end-to-end request cost or a delivery guarantee.
evlog vs alternatives
The comparison suite runs libraries with different output paths. At commit 5bd1c127, the configurations are:
| Library | Work included in an emitted record | Work excluded |
|---|---|---|
| evlog | Event construction with silent: true | Console serialization, console writes, remote drains |
| Pino | JSON serialization and synchronous write to /dev/null | Remote transport and indexing |
| Winston | JSON formatting and a no-op writable stream | Remote transport and indexing |
| Consola | Processing through a no-op reporter | Reporter serialization and delivery |
These configurations help inspect individual code paths. They do not isolate the cost of choosing one library over another. /dev/null avoids a stored file, but its write still includes a system call.
Results
Earlier versions of this page ranked silent: true construction against serialized output as though they performed equivalent work. Those rankings are withdrawn. The suite's request-lifecycle scenario also compares one accumulated evlog event with four Pino or Winston records, so output count and serialization differ at the same time.
Generate results for the checked-out version using the commands below. Keep the raw results alongside the commit, lockfile, Node.js version, hardware, and logger configuration. A result without that context is not a production throughput promise.
What is the "wide event lifecycle"?
The scenario creates a logger, adds three groups of fields, and emits a final event. A multi-record version writes after each group and at completion. Moving from four records to one reduces the event count by 75% for that scenario. It does not establish a universal reduction in bytes, CPU time, or cost.
Pino and Winston can also emit one final object. For a library comparison, measure that case too. The log cost guide separates event-based and byte-based billing and lets you use your own traffic assumptions.
What affects event construction?
log.set() merges fields into the pending context. emit() selects severity, applies sampling and configured redaction, and prepares the final event. With JSON console output enabled, serialization runs during output. A drain can serialize again for its own protocol.
The amount and shape of data matter: nested objects, error stacks, redaction patterns, and plugin hooks exercise different paths. Timing a small event without output does not price a large event with custom enrichment and export.
When evlog might not win
A Pino transport can run in a worker, separating transport processing from the calling thread. Initial record serialization still happens in the logger. Winston formats and transports, or a Consola reporter, may already implement the exact output your application needs. Keeping that configuration can be preferable to migrating it.
There is no supported claim here that evlog wins every other workload. Measure throughput and application latency with your destination, payloads, and delivery policy. Include queued work and final flushing, not just the time it takes to enqueue an event.
What it costs on one real request
Measure the complete path you deploy:
- Run a representative handler with
enabled: falseto establish a baseline without logging. - Enable event construction, the redaction rules and enrichers you use, and the actual output configuration.
- Exercise successes, errors, and the largest expected context. Record request latency distributions as well as throughput.
- Include batching, retries, queue limits, and shutdown or background-delivery behavior. Confirm records reach the destination.
Do not add means from unrelated microbenchmarks and label their sum a measured request cost. Work may overlap, take different branches, or happen asynchronously. Network delivery and backend indexing need their own measurements.
Bundle size
An emitted entry-point file is not the size of the application bundle that imports it. Shared chunks and selected exports contribute to the final size. The server core, evlog/http, framework integrations, and drain adapters also target different uses.
Build the entry points you plan to import and measure the resulting bundle. For a comparison with another library, keep the bundler, target, minification, compression, versions, and imported capabilities consistent. A server bundle cannot establish which browser logger is smaller.
Detailed benchmarks
| Suite | Question it helps answer |
|---|---|
bench/core/ | How do context size, merging, emission, and errors affect construction? |
bench/comparison/ | What do the explicitly configured alternative workloads cost? |
Other files under bench/ | What do the corresponding enrichers, middleware, sampling, or pipeline operations cost? |
Read each setup before interpreting a result: silent output, disabled drains, and reused fixtures can change the work included. Sampling and Configuration describe the controls you should match to your application.
Methodology & trust
Can you trust these numbers?
The benchmark source is available for inspection. A trustworthy comparison additionally needs equivalent work, versioned inputs, and reproducible output. The current comparison setup does not meet the equivalent-output condition.
When publishing a run, record the commit, dependency versions, runtime, CPU, operating system, warmup and measurement settings, payload, redaction, and destination. Repeat it to expose variation. State whether the number measures construction, serialization, queued delivery, or completed delivery.
Run it yourself
From a checkout with dependencies installed, run the suite once rather than leaving it in watch mode:
cd packages/evlog
pnpm exec vitest bench --run
pnpm exec vitest bench --run bench/comparison/
pnpm exec tsx bench/scripts/size.ts
Vitest writes benchmark results to bench/results.json. Keep that output with the run metadata when sharing a measurement. The size script measures its declared imports, so inspect them before applying the result to your own bundle.