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
A blazing-fast structured logger for Node and Bun — about 4× faster than Pino, with zero-config TypeScript types.
$ npm install cinder
TRUSTED IN PRODUCTION AT
The whole API, basically
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.
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')
{ "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
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
What's in the box
No grab-bag of half-features. Each one is load-bearing in production and has a benchmark to back it.
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
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' })
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 })
Pass a list of paths and Cinder masks them before serialization — authorization, card.number, wildcards included. Secrets never reach the buffer.
redact: ['*.password']
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([...])
Survives tsx watch, Bun's --hot and Next.js Fast Refresh without leaking worker threads or duplicating transports across reloads.
tsx watch ./server.ts
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.
The benchmark
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.
How it compares
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
Three steps. No config file, no tsconfig changes, no @types install.
One package, zero peer dependencies. Bun users swap npm for bun.
$ npm install cinder
Import, call cinder(), log structured fields. The return type is fully typed.
import { cinder } from 'cinder'
const log = cinder({ name: 'app' })
log.info({ port: 3000 }, 'server up')
Pipe through cinder-pretty in dev. Production gets raw ndjson.
$ node app.ts | npx cinder-pretty
# 12:04:18 INFO app
# server up port=3000
Community
Eleven thousand developers in chat, a discussions board where the median first reply is under three hours, and four maintainers who actually read it.
CORE MAINTAINERS
The four who triage issues, cut releases, and own the benchmark.
Sponsors
GitHub Sponsors funds the maintenance hours, the benchmark hardware, and the CI minutes. No VC, no paid tier, no telemetry.
PLATINUM SPONSORS
INDIVIDUAL SPONSORS
28 developers · from $3/mo
FAQ
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.
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.
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.
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.
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.
One install, one import, and your logs are structured JSON at 148ns a call. No config file to write first.
$ npm install cinder