---
title: "evlog vs Pino: request context and migration"
description: "Compare request context, serializers, transports, and migration costs before moving a Pino application to evlog."
canonical_url: "https://www.evlog.dev/compare/pino"
---
# evlog vs Pino: request context and migration

> Compare request context, serializers, transports, and migration costs before moving a Pino application to evlog.

Your Pino setup already ships JSON logs. The next decision is how your application assembles request context. evlog provides an accumulator and a lifecycle for that context. Pino provides JSON logging, bindings, serializers, and transports that you can keep if they already fit your application.

## Compare the work you maintain

<table>
<thead>
  <tr>
    <th>
      Concern
    </th>
    
    <th>
      evlog
    </th>
    
    <th>
      Pino
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      Request context
    </td>
    
    <td>
      <code>
        createLogger()
      </code>
      
       and typed <code>
        set()
      </code>
      
       calls accumulate fields until <code>
        emit()
      </code>
    </td>
    
    <td>
      <code>
        child()
      </code>
      
       attaches bindings to records. The application can accumulate an object and log it once.
    </td>
  </tr>
  
  <tr>
    <td>
      Adding bindings
    </td>
    
    <td>
      <code>
        set()
      </code>
      
       merges fields into the pending event
    </td>
    
    <td>
      <code>
        setBindings()
      </code>
      
       adds bindings after creation. It does not overwrite existing keys and can introduce duplicate keys.
    </td>
  </tr>
  
  <tr>
    <td>
      Error fields
    </td>
    
    <td>
      <code>
        createError()
      </code>
      
       carries <code>
        why
      </code>
      
      , <code>
        fix
      </code>
      
      , and <code>
        link
      </code>
    </td>
    
    <td>
      Error serializers, including <code>
        pino.stdSerializers.err
      </code>
      
      , shape error records.
    </td>
  </tr>
  
  <tr>
    <td>
      Extensibility
    </td>
    
    <td>
      Plugins and enrichers hook into the event lifecycle
    </td>
    
    <td>
      Per-key serializers, formatters, and transports
    </td>
  </tr>
  
  <tr>
    <td>
      Levels
    </td>
    
    <td>
      Six severities (<code>
        trace
      </code>
      
       to <code>
        fatal
      </code>
      
      ), global <code>
        minLevel
      </code>
      
      , sampling for wide events
    </td>
    
    <td>
      Built-in and custom levels, mutable logger level
    </td>
  </tr>
  
  <tr>
    <td>
      Delivery
    </td>
    
    <td>
      Drain adapters and configurable batching/retry
    </td>
    
    <td>
      Destinations and transports, including worker-thread transports
    </td>
  </tr>
</tbody>
</table>

Pino documents these contracts in its [API reference](https://github.com/pinojs/pino/blob/main/docs/api.md). A worker transport moves transport processing and delivery to a worker. The logger still performs the initial JSON serialization on the calling thread. See [Pino transports](https://github.com/pinojs/pino/blob/main/docs/transports.md).

## Compare one completed operation

Both libraries can emit one structured record for an operation. Install `pino` or `evlog` and run the corresponding TypeScript file with Node.js 24. The `decline` argument exercises the failure path. These examples simulate the same checkout decision without calling a payment provider.

With Pino, the application owns the accumulated context and emits it in `finally`:

```typescript [checkout-pino.ts]
import pino from 'pino'

const logger = pino()
const context = { orderId: 'order-123', amount: 2999, outcome: 'pending' }
let failure: Error | undefined

try {
  if (process.argv.includes('decline')) throw new Error('Payment declined')
  context.outcome = 'paid'
} catch (error) {
  failure = error instanceof Error ? error : new Error(String(error))
  context.outcome = 'declined'
  process.exitCode = 1
} finally {
  if (failure) logger.error({ ...context, err: failure }, 'checkout completed')
  else logger.info(context, 'checkout completed')
}
```

With evlog, the logger owns the accumulator and selects error severity after `error()`:

```typescript [checkout-evlog.ts]
import { createLogger, initLogger } from 'evlog'

initLogger({ env: { service: 'checkout' }, pretty: false })
const log = createLogger({ orderId: 'order-123', amount: 2999, outcome: 'pending' })

try {
  if (process.argv.includes('decline')) throw new Error('Payment declined')
  log.set({ outcome: 'paid' })
} catch (error) {
  log.error(error instanceof Error ? error : new Error(String(error)))
  log.set({ outcome: 'declined' })
  process.exitCode = 1
} finally {
  log.emit()
}
```

Each version emits one record on either path. Their envelopes differ: Pino uses a numeric level and its error serializer, while evlog adds its event metadata and duration. evlog's [wide-event model](https://www.evlog.dev/learn/wide-events) becomes useful when several layers contribute fields and framework integrations own the request lifecycle. Pino's `pino-http` also provides automatic HTTP request logging.

## Migrate one handler before changing the whole application

Inventory the fields your dashboards query, custom levels, serializers, and transport configuration. Map them explicitly before switching output. Pino serializers do not transfer unchanged into evlog plugins, and a custom transport may need a new drain.

Keep the existing Pino path for unrelated handlers while you [validate one evlog integration](https://www.evlog.dev/integrate/frameworks/overview). Avoid emitting duplicate records for the same request through both paths unless you deliberately account for that in queries and ingestion costs. Test successful requests, thrown errors, shutdown, and redaction using the same assertions about required fields.

## Read benchmark conditions before choosing on speed

The repository's historical comparison uses silent evlog event construction, Pino JSON writes to `/dev/null`, and different output counts in the request-lifecycle scenario. It does not establish an equivalent-output throughput ranking. The [Performance reference](https://www.evlog.dev/reference/performance) records the workloads and how to measure your own configuration.

Keep Pino when its serializers, ecosystem integrations, or worker transports already solve your problem. Choose evlog when its context accumulator, structured errors, and framework lifecycle remove application code you would otherwise maintain. If you chose evlog, start with the [framework integrations](https://www.evlog.dev/integrate/frameworks/overview). For other choices, use [evlog vs other loggers](https://www.evlog.dev/reference/vs-other-loggers).

---

- [Performance Benchmarks](https://www.evlog.dev/reference/performance)
- [vs Other Loggers](https://www.evlog.dev/reference/vs-other-loggers)
- [Wide Events](https://www.evlog.dev/learn/wide-events)


## Sitemap

See the full [sitemap](https://www.evlog.dev/sitemap.md) for all pages.
