v3.2 · Released Mar 14, 2026 · MIT

Structured logging at 148ns per log call.

A blazing-fast structured logger for Node and Bun — about 4× faster than Pino, with zero-config TypeScript types.

npm install cinder
4.4M weekly downloads 0 dependencies 8.1 kB minzipped 84 contributors

TRUSTED IN PRODUCTION AT

Microsoft ▲ Vercel Linear Supabase stripe Railway Fly.io PlanetScale Resend

The whole API, basically

Log once. Read structured JSON.

A logger, a structured call, and a child that inherits context — then the exact line-delimited JSON Cinder writes to stdout. No formatter plugin required.

logger.ts
1import { cinder } from 'cinder'
2
3const log = cinder({ level: 'info', name: 'api' })
4
5// structured fields first, message last
6log.info(
7  { route: '/checkout', userId: 4471, ms: 28 },
8  'request handled',
9)
10
11// child loggers inherit context — zero overhead
12const orderLog = log.child({ orderId: 'ord_9F2A' })
13orderLog.warn({ retries: 2 }, 'payment provider slow')
stdout · ndjson
2 LINES
{ "level":"info", "time":1742000128411,
  "name":"api", "route":"/checkout",
  "userId":4471, "ms":28,
  "msg":"request handled" }

{ "level":"warn", "time":1742000128439,
  "name":"api", "orderId":"ord_9F2A",
  "retries":2,
  "msg":"payment provider slow" }

// piped to `cinder-pretty` in dev,
// raw ndjson straight to your log sink in prod.

Why Cinder exists

We love Pino. We just wanted it faster.

Cinder started as a benchmark we couldn't stop tuning. Three years later it runs in a few large fleets.

Pino is excellent, and Cinder owes it a debt. The line-delimited JSON convention, the "fields first, message last" call signature, the transport-in-a-worker idea — Pino proved all of it, and we copied the parts that were right. Cinder is not a rebellion against Pino. It is what happens when you take Pino's design and spend three years removing every allocation on the hot path.

The result is roughly 4× faster — 148ns per log call versus 580ns on the same machine, same Node 22, same payload. That gap is not magic: Cinder pre-compiles a serializer per logger shape, reuses a ring buffer instead of allocating strings, and never touches JSON.stringify on the common path. When a request handler logs four times, that's the difference between 2.3µs and 0.6µs of pure logging overhead.

And it is zero-config TypeScript — no @types/cinder to install, no version skew between the runtime and its types, no declare module patching. The types ship in the package, they're generated from the same source, and log.child() returns a logger whose context type actually narrows. Child loggers compose without overhead because a child is a frozen object sharing one serializer with its parent.

THINGS CINDER DELIBERATELY DOESN'T DO

  • No pretty-printing in the core. Formatting is a separate cinder-pretty binary. The core writes ndjson, full stop.
  • No log levels you invented. Six fixed levels — trace, debug, info, warn, error, fatal. Custom levels fragment dashboards.
  • No string-interpolation API. No log('user %s', id). Structured fields or nothing — printf logging isn't queryable.
  • No global singleton. No cinder.configure() mutating shared state. You pass a logger; we don't hide one in a module.
  • No plugin marketplace. Transports are a 12-line interface. We'd rather you write one than browse forty.

What's in the box

Seven things, done properly.

No grab-bag of half-features. Each one is load-bearing in production and has a benchmark to back it.

148ns per log call

A pre-compiled serializer per logger shape and a reused ring buffer. We re-run the benchmark in CI on every commit and fail the build if it regresses past 165ns.

npx cinder bench --runs 1e6

Line-delimited JSON

One JSON object per line, straight to stdout. Ships to Loki, Datadog, CloudWatch or a flat file with zero adapters. Field order is stable, so diffs stay readable.

cinder({ format: 'ndjson' })

Child loggers that compose

A child is a frozen object sharing one serializer with its parent — no copy, no re-compile. Bind a requestId once and every line downstream carries it.

log.child({ requestId })

Redaction, declared once

Pass a list of paths and Cinder masks them before serialization — authorization, card.number, wildcards included. Secrets never reach the buffer.

redact: ['*.password']

Transport pipeline

Fan a log stream into a worker thread, a file, an HTTP sink and your console at once. Transports run off the hot path, so a slow sink never blocks a request.

cinder.transport([...])

Hot-reload safe

Survives tsx watch, Bun's --hot and Next.js Fast Refresh without leaking worker threads or duplicating transports across reloads.

tsx watch ./server.ts

Bun, Node and Deno — one package

The same cinder import runs on Node 18+, Bun 1.1+ and Deno 2 without a build flag or a runtime shim. Cinder probes the runtime once at import and picks the fastest available write path — Bun's native fast-writer when present, a libuv stream otherwise.

148ns
Node 22
131ns
Bun 1.2
172ns
Deno 2

The benchmark

148ns. Measured a million times.

Time per .info() call with a six-field payload, Node 22.3 on an Apple M3, one million iterations, median of 11 runs. Same harness for every logger — it lives at /bench in the repo, run it yourself.

3.9×
faster than Pino
6.7M
logs / sec / core
0
heap allocs / call
NS PER LOG CALL · LOWER IS BETTER

How it compares

Cinder next to the usual suspects.

All four are real, all four work. This is where they differ when you're picking one for a service that logs millions of lines a day.

Capability
Cinder
Pino Winston console.log
Time per log call 148ns 580ns 1,640ns ~7,400ns
TypeScript-native types In-package JS-first, @types JS-first, @types
Built-in redaction Via format
Transport pipeline Transports
Child loggers Zero-overhead Via .child()
First-class Bun support Works, untuned Works, untuned Built-in

Numbers from the shared /bench harness, Node 22.3 / M3, Mar 2026. Pino remains the closest peer — and the one we benchmark against in CI.

Quick start

Hello world in under 60 seconds.

Three steps. No config file, no tsconfig changes, no @types install.

1

Install it

One package, zero peer dependencies. Bun users swap npm for bun.

terminal
$ npm install cinder
2

Make a logger

Import, call cinder(), log structured fields. The return type is fully typed.

app.ts
import { cinder } from 'cinder'

const log = cinder({ name: 'app' })
log.info({ port: 3000 }, 'server up')
3

Run & pretty-print

Pipe through cinder-pretty in dev. Production gets raw ndjson.

terminal
$ node app.ts | npx cinder-pretty
# 12:04:18  INFO  app
#   server up  port=3000

Community

Built in the open, answered fast.

Eleven thousand developers in chat, a discussions board where the median first reply is under three hours, and four maintainers who actually read it.

GitHub Discussions
1,940

threads · 96% answered · median first reply 2h 51m

Open the board
Discord 1,284 online
11,206

members · #help, #perf, #transports, #bun

Join the server
Contributors
84

across 9 minor releases · 2,310 merged PRs

Good first issues

CORE MAINTAINERS

The four who triage issues, cut releases, and own the benchmark.

Sponsor the project
RV
Renata Vasquez
@renvsq · lead
JK
Jonas Kessler
@jkessler · transports
AO
Amara Okafor
@amaraok · types
TN
Takeshi Nakamura
@tknakam · perf

FAQ

The questions we actually get.

Is Cinder production-ready?

Yes, and it has been for a while. Cinder v1.0 shipped in mid-2024; v3.2 is the current line. It runs in production at Microsoft, Vercel and Linear, serves 4.4M weekly npm downloads, and follows semantic versioning strictly — no breaking change has ever landed in a minor release. The API surface is small and frozen: cinder(), .child(), six log methods, and the transport interface. We treat that contract as load-bearing.

Why not just use Pino?

Pino is great — if it's already in your stack and working, there is no urgent reason to switch. Cinder is worth it on three counts: it's roughly 4× faster per call (148ns vs 580ns), which matters once a service logs millions of lines a day; its TypeScript types ship in the package and genuinely narrow through .child(), so there's no @types skew; and redaction plus the transport pipeline are in core rather than bolted on. If none of those pinch you, stay on Pino with our blessing — we benchmark against it precisely because we respect it.

Are the TypeScript types actually accurate?

They're first-class, not an afterthought. There is no separate @types/cinder package, so there is no version skew between the runtime and its types — they're generated from the same source and shipped together. .child() returns a logger whose bound-context type is intersected with the parent's, the redaction paths are checked against your payload shape, and the six log levels are a literal union. We run tsd assertions in CI against TypeScript 5.3 through 5.7, so a type regression fails the build the same way a runtime one does.

How fast is "fast" — what's the benchmark?

148ns is the median time for one .info() call carrying a six-field object payload, on Node 22.3 running on an Apple M3, over one million iterations, taking the median of 11 runs. The exact same harness runs every logger — Pino lands at 580ns, Winston at ~1,640ns — and it lives at /bench in the repo, so you can clone it and reproduce the numbers on your own hardware. CI re-runs it on every commit and fails if Cinder regresses past 165ns. We don't publish a number we can't reproduce on demand.

Does it support OpenTelemetry?

Yes, through the transport pipeline. The official @cinder/otel transport ships log records to an OTLP collector and, when an active span is in context, automatically stamps each line with its trace_id and span_id so logs and traces correlate in your backend. It's an opt-in package, not a core dependency — the core stays at zero dependencies. Metrics and traces are out of scope by design: Cinder is a logger, and it expects to sit beside the OpenTelemetry SDK, not replace it.

v3.2 · MIT · 0 dependencies

Get started in 60 seconds.

One install, one import, and your logs are structured JSON at 148ns a call. No config file to write first.

terminal
$ npm install cinder