Skip to content

Throughput

shorn is faster than byte-producing JSON in every fixture measured. Against the binary codecs it leads on record-shaped data. It does not lead on document-shaped data: a payload made of many separate strings is decoded faster by msgpackr’s bundleStrings mode, whatever alphabet those strings use.

Tests ran on Node v24.18.0, an Apple M4 Pro, and macOS 26.6.2 arm64. Every table on this page comes from one run of pnpm bench, the small-fixture suite. Results from different runs or machines are not comparable, so they are never mixed in one table.

Small-fixture results are the median of seven samples of about 180 ms each, after warm-up. The 100,000-event results, published under payload size, use three single-operation samples, because one operation already processes the whole value. Every codec has to round-trip to the same logical value.

Schema construction is excluded here and measured separately as cold setup. Raw tests use each codec’s normal API with SchemaPack validation disabled. Protobuf.js includes fromObject and toObject so that it exposes the same string-enum API as the others.

Object schemas that qualify use generated encode and decode functions. A strict Content Security Policy falls back to the interpreted path with identical bytes and results. See Compilation and caching for which schemas take which path.

JSON bytes converts to and from a Uint8Array, which makes it the direct comparison for a binary transport.

Fixtureshorn encJSON encshorn decJSON dec
Person23.07M4.21M66.26M5.10M
Unicode person7.76M3.01M8.27M3.92M
Nested event8.54M1.28M12.35M1.84M
100-event batch97.3k29.7k121.3k22.3k
Person, validated8.49M3.46M11.92M3.88M

Across these fixtures shorn is up to 6.7× faster to encode, on the nested event, and up to 13.0× faster to decode, on Person. The ASCII payloads also use as little as 23% of JSON’s bytes. The Unicode payload uses 53%.

JSON.stringify to a string reaches 9.34M encodes/s for Person. That baseline does less work, because it stops at a JavaScript string rather than producing bytes.

msgpackr has three modes. This table compares against one of them. bundleStrings is measured in the document section below, where it wins decode outright.

FixtureOpshornAvroSchemaPackmsgpackr records
Personenc23.07M6.32M8.03M10.01M
Persondec66.26M20.77M12.25M19.24M
Unicode personenc7.76M3.85M5.61M7.36M
Unicode persondec8.27M8.71M6.70M3.76M
Nested eventenc8.54M3.53M2.91M3.53M
Nested eventdec12.35M4.49M4.08M8.11M
100 eventsenc97.3K36.1K35.2K26.0K
100 eventsdec121.3K44.2K45.8K89.6K

The fixtures above are records: few keys, short strings, no optional fields. A document, meaning many keys, mostly string content, and arrays whose elements have different key sets, measures differently:

CodecBytesEncodeDecode
shorn2,218266.9K294.1K
msgpackr shared records2,250150.2K478.1K
msgpackr bundled strings2,316166.1K607.5K
cbor-x shared records2,303145.2K444.1K
@msgpack/msgpack2,85491.6K94.5K
JSON bytes3,316183.3K175.1K

shorn is smallest and fastest to encode, by 46% over the next codec, which here is JSON bytes, and fourth to decode. The cause is one string-decode call per string. bundleStrings writes all string content into one contiguous region and decodes it in a single call. shorn pays that call once per string, and 87% of this payload is string bytes spread across 88 separate strings.

The alphabet has nothing to do with it. The fixture is pure ASCII and shorn still decodes it at 48% of bundleStrings. What costs is the number of strings, not what is in them.

The remaining gap is a wire-format question rather than a tuning one: bundling strings would change the bytes.

CodecBytesEncodeDecode
shorn + Zod88.49M11.92M
Zod + Avro84.30M8.75M
Zod + SchemaPack95.50M6.60M
Zod + JSON string355.99M4.79M
Zod + JSON bytes353.46M3.88M

Validation is most of the end-to-end cost, and it narrows the field. On Person, shorn’s raw lead over Avro is 3.6× encoding and 3.2× decoding; with Zod on every value it comes down to 2.0× and 1.4×, because the validator is then most of the work. Earlier recordings flattened the encode column to a tie with Avro. This run does not: shorn stays 97% ahead on encode and 36% ahead on decode.

On the Person fixture the raw codec runs at 23.07M encodes/s and 66.26M decodes/s. Adding Zod brings those down to 8.49M and 11.92M.

Between services you own, unchecked(compile(schema)) writes the same bytes at the raw-codec speed, giving up every refinement on both sides in exchange. See Skipping validation.

Terminal window
pnpm bench
pnpm bench:all

Benchmark your own schemas and traffic before making a production decision.