Benchmarks
Numbers from the codec-benchmarks module (JMH), so the guides' speed and
size claims are reproducible rather than folklore. One machine, one run:
Apple M4 Max, 64 GB, OpenJDK 25.0.3, codec 0.8.0-SNAPSHOT, 2026-09-06.
Throughput mode, 1 fork, 3 × 1 s warm-up, 5 × 1 s measurement. The raw JMH
output is in codec-benchmarks/results/; treat the relative ordering as the
finding, not the absolute figures.
Compression payloads: small is a 104-byte JSON record, medium an 8 KB
JSON document, large 1 MB of pseudo-English prose. Backend payloads: a
small four-field record and a medium order with 100 line items (nested
records, a list, strings and numbers).
Deflate is measured at levels 1, 6 (the default) and 9; gzip shares its engine
and adds only framing, so it appears at the default level alone. zstd is
measured at 1, 3 (the default), 9 and 19.
What the numbers say
- For medium and large payloads, zstd and LZ4 are in a different league
from the JDK. zstd 3 encodes 1 MB at ~620 MB/s against deflate 6's ~38 MB/s
and decodes two and a half times as fast; LZ4 encodes at ~790 MB/s and
decodes at ~1.9 GB/s. Even the JDK's fastest level (deflate 1, ~250 MB/s) is
well behind zstd 1 (~650 MB/s) while producing a worse ratio (26.0% vs 23.2%).
- For tiny payloads the JDK wins. At 104 bytes gzip and deflate out-run
zstd and LZ4 on both encode and decode — the fixed cost of a JNI stream
dominates, and there is nothing to compress anyway (every transform grows
the small payload).
- Levels buy ratio slowly and cost speed quickly. deflate 9 is ~45% slower
than deflate 6 for 0.4 points of ratio; zstd 9 is ~6× slower than zstd 3 for
0.5 points; zstd 19 encodes at 6 MB/s for the best ratio of all (16.2%).
The defaults are defaults for a reason.
- Ratio is not zstd's advantage at the default level. On the prose payload
zstd 3 lands at 21.7% of the input against deflate 6's 19.2%; only zstd 19
beats it. zstd's win is speed at a comparable ratio.
- LZ4-HC is a decode-side optimisation. It compresses ~22× slower than
plain LZ4 (about gzip's speed) for a better ratio, and decodes fastest of
anything measured. Use it for write-rarely, read-constantly data.
- The text encodings are negligible next to any backend or transform.
Base64 and hex run on the JDK's intrinsics; the pure-Java Base32 encodes at
~1.8 GB/s and decodes at ~1.5 GB/s, about eight times slower than Base64 and
well clear of anything it would be chained behind.
- Binary backends pull further ahead as the payload grows. On the small
record Fory decodes ~5× faster than Jackson 2; on the 100-item order it is
~10× faster on decode and ~15× on encode (1.4 M orders/s), and Protobuf is
~5× faster than the JSON backends. Their output is also half the size:
4.5 KB (Fory) and 4.8 KB (Protobuf) against 9.1 KB of JSON.
- Fory's compatible mode costs bytes only on tiny payloads.
codec-fory
takes Fory's default, compatible mode (see the
Fory guide), which writes each class's
schema once per message: the lone four-field record
is 74 bytes, no smaller than its 72 bytes of JSON, while the 100-item order
pays 2%. Throughput is unaffected.
- Among the JSON backends, Jackson 2 decodes fastest in this run — almost
twice as fast as Jackson 3 on the order — and JSON-B (Yasson) is the slowest
at both sizes.
BoundedDataKeyStrategy more than doubles encrypt throughput on small
payloads (1.0 M vs 0.42 M ops/s) by amortising the KEK wrap; decode is
unchanged, because every message is unwrapped on its own.
Encoded sizes by backend
From EncodedSizes, committed at
codec-benchmarks/results/2026-09-06-encoded-sizes.md:
| Backend |
small (bytes) |
medium (bytes) |
| jackson3 |
72 |
9,096 |
| jackson2 |
72 |
9,096 |
| gson |
72 |
9,096 |
| jsonb |
72 |
9,096 |
| fory |
74 |
4,510 |
| protobuf |
35 |
4,815 |
Throughput
Compression tables carry the compressed size and ratio beside the speed, so
the level trade-offs read in one place.
Compression — small payload (104 bytes)
| Transform |
compressed bytes |
of input |
encode MB/s |
decode MB/s |
encode ops/s |
decode ops/s |
| gzip |
115 |
110.6% |
27 |
61 |
263,438 |
584,761 |
| deflate1 |
103 |
99.0% |
27 |
63 |
255,848 |
608,249 |
| deflate6 |
103 |
99.0% |
26 |
63 |
252,615 |
604,734 |
| deflate9 |
103 |
99.0% |
27 |
63 |
255,637 |
608,106 |
| zstd1 |
102 |
98.1% |
17 |
21 |
161,182 |
204,964 |
| zstd3 |
102 |
98.1% |
11 |
17 |
102,589 |
162,277 |
| zstd9 |
102 |
98.1% |
2 |
17 |
20,516 |
167,031 |
| zstd19 |
103 |
99.0% |
0 |
16 |
2,991 |
154,408 |
| lz4 |
123 |
118.3% |
41 |
49 |
392,272 |
468,573 |
| lz4hc |
123 |
118.3% |
29 |
50 |
278,798 |
483,448 |
Compression — medium payload (8,338 bytes)
| Transform |
compressed bytes |
of input |
encode MB/s |
decode MB/s |
encode ops/s |
decode ops/s |
| gzip |
1,647 |
19.8% |
218 |
967 |
26,098 |
115,925 |
| deflate1 |
1,846 |
22.1% |
496 |
808 |
59,532 |
96,957 |
| deflate6 |
1,635 |
19.6% |
212 |
853 |
25,413 |
102,297 |
| deflate9 |
1,606 |
19.3% |
159 |
854 |
19,080 |
102,468 |
| zstd1 |
1,614 |
19.4% |
561 |
692 |
67,264 |
83,003 |
| zstd3 |
1,590 |
19.1% |
377 |
772 |
45,171 |
92,633 |
| zstd9 |
1,508 |
18.1% |
97 |
897 |
11,590 |
107,549 |
| zstd19 |
1,467 |
17.6% |
7 |
889 |
881 |
106,610 |
| lz4 |
2,704 |
32.4% |
1,052 |
1,532 |
126,206 |
183,791 |
| lz4hc |
2,335 |
28.0% |
381 |
1,622 |
45,741 |
194,508 |
Compression — large payload (1,048,576 bytes)
| Transform |
compressed bytes |
of input |
encode MB/s |
decode MB/s |
encode ops/s |
decode ops/s |
| gzip |
201,172 |
19.2% |
38 |
756 |
36 |
721 |
| deflate1 |
273,057 |
26.0% |
248 |
495 |
236 |
472 |
| deflate6 |
201,160 |
19.2% |
38 |
677 |
36 |
646 |
| deflate9 |
197,182 |
18.8% |
20 |
700 |
20 |
667 |
| zstd1 |
242,999 |
23.2% |
648 |
1,561 |
618 |
1,488 |
| zstd3 |
227,860 |
21.7% |
621 |
1,755 |
593 |
1,673 |
| zstd9 |
222,392 |
21.2% |
95 |
1,835 |
91 |
1,750 |
| zstd19 |
169,684 |
16.2% |
6 |
2,113 |
6 |
2,015 |
| lz4 |
491,989 |
46.9% |
792 |
1,886 |
755 |
1,799 |
| lz4hc |
282,916 |
27.0% |
36 |
2,549 |
35 |
2,431 |
Encodings and checksum (medium payload, 8 KB)
| Transform |
encode ops/s |
decode ops/s |
| base64 |
1,787,991 |
1,148,057 |
| base32 |
220,631 |
182,517 |
| hex |
406,702 |
104,500 |
| crc32c |
1,091,752 |
1,160,519 |
Backends — one small record
| Backend |
encode ops/s |
decode ops/s |
| jackson3 |
7,826,665 |
3,575,335 |
| jackson2 |
8,874,575 |
6,998,820 |
| gson |
5,550,844 |
3,832,645 |
| jsonb |
4,099,414 |
1,552,030 |
| fory |
43,554,798 |
36,590,914 |
| protobuf |
73,836,647 |
28,808,969 |
Backends — an order with 100 line items
| Backend |
encode ops/s |
decode ops/s |
| jackson3 |
97,993 |
35,863 |
| jackson2 |
90,473 |
64,103 |
| gson |
56,377 |
40,996 |
| jsonb |
49,764 |
23,859 |
| fory |
1,385,881 |
674,915 |
| protobuf |
524,202 |
355,915 |
Envelope encryption
| Strategy |
Payload |
encode ops/s |
decode ops/s |
| direct |
small |
415,616 |
494,701 |
| bounded |
small |
1,017,118 |
501,934 |
| direct |
medium |
229,782 |
245,527 |
| bounded |
medium |
329,200 |
245,931 |
Running them yourself
./mvnw -q -pl codec-benchmarks -am package -DskipTests
java --enable-native-access=ALL-UNNAMED -jar codec-benchmarks/target/benchmarks.jar \
-rf json -rff results.json
java -cp codec-benchmarks/target/benchmarks.jar org.jwcarman.codec.benchmarks.CompressionRatios --json > ratios.json
java -cp codec-benchmarks/target/benchmarks.jar org.jwcarman.codec.benchmarks.EncodedSizes
python3 codec-benchmarks/render.py results.json ratios.json # the throughput tables
Run a subset by name (CompressionBenchmark) or parameter (-p codec=zstd3).
The module is not published and not part of CI — benchmark numbers from shared
runners are noise.
Where next
- Codec Composition — the transforms these numbers
measure, and how to choose one
- Apache Fory — the backend these numbers show pulling
furthest ahead on JVM-to-JVM payloads
- Encryption — the envelope-encryption strategies
benchmarked above