Redisson Codec Benchmark: What Kryo5, Fory, JSON and Compression Actually Cost

Published on
September 7, 2026

Every object you put into Valkey or Redis from a Java application passes through a codec on the way out and on the way back — the component that handles serialization so the server, which knows nothing about Java, can store plain bytes. Our guide to choosing the right Redisson codec covers what each one is for and when to reach for it. What it could not tell you is what the choice costs.

So we measured all of them. This article is the numbers: how long each codec takes to encode and decode, how many bytes it puts on the wire, and how much garbage it leaves behind — for six payload shapes, from a boxed long to a thousand-element list.

The short version, before the tables. ForyCodec is faster than Kryo5Codec on every payload we measured, by 1.7–4.2×, and costs about 4 % more bytes on values of real size. JSON costs roughly an order of magnitude over binary. And on the compression side, the interesting question turns out not to be which compressor is fastest but whether your data compresses at all.

How This Was Measured

Benchmark numbers are only worth as much as the method behind them, so here is the method first.

Every codec is driven through the Encoder and Decoder pair that Redisson itself calls, not through the underlying library's own API. That distinction matters more than it sounds. A serialization library benchmarked in isolation never allocates the Netty buffer that a real client has to allocate, never pays for the copy into it, and never tells you how many bytes actually reach the server. All three are inside these measurements.

The harness is JMH 1.37 running AverageTime mode, two forks of three one-second warmup iterations followed by five one-second measurement iterations, on OpenJDK 21. The encode benchmark allocates its output buffer from ByteBufAllocator.DEFAULT exactly as the codec does in production. The decode benchmark rewinds one pre-filled buffer rather than allocating a fresh one, so it measures decoding and nothing else.

Before any timing runs, every codec and payload pair is round-tripped and compared against the original. A benchmark of a silently failing path measures nothing, and this is the cheapest way to be sure that is not what is happening.

Three numbers are reported for each codec, because a cache pays for all three: time, bytes on the wire, and bytes allocated per operation. Only the first appears in most published benchmarks, and it is arguably the least important of the three for a system whose bill scales with memory and bandwidth.

The six payloads are a boxed long, a 64-character string, a six-field bean, a nested bean graph holding another bean plus two lists and a map, an ArrayList of 1000 strings, and a HashMap of 100 beans. Three more redundant payloads join for the compression section, where the shape of the data matters more than its size.

One caveat worth stating plainly: these ran on a modest two-core virtual machine. Treat the ratios between codecs as the result and the absolute nanoseconds as indicative — on server hardware every number here will be lower, but the relationships between them hold.

Kryo5Codec vs ForyCodec

Kryo5Codec is Redisson's default and the codec most applications are using without having thought about it. ForyCodec is the newer alternative, built on Apache Fory. Both are compact binary formats that handle arbitrary Java object graphs without a schema. Here is what they cost, out of the box with no classes declared.

PayloadEncode Kryo5Encode ForyDecode Kryo5Decode ForyBytes Kryo5Bytes Fory
Boxed long232 ns138 ns172 ns52 ns912
String, 64 chars306 ns152 ns214 ns80 ns6769
Flat bean, 6 fields346 ns182 ns404 ns96 ns92136
Nested bean graph1 423 ns429 ns1 883 ns749 ns451539
ArrayList, 1000 strings21 506 ns11 509 ns30 973 ns17 870 ns24 02525 007
HashMap, 100 beans13 799 ns4 353 ns17 668 ns7 236 ns6 2746 600

Fory wins every timing. Encoding is 1.7–3.3× faster and decoding 1.7–4.2× faster, with the widest gap on the flat bean — 96 ns against 404 ns.

The decode column is the one to weigh most heavily. A cache is read more often than it is written, frequently by a large margin, so a codec's decode cost is multiplied by your read rate. It is also the cost that sits directly in the path of a user request, which is why it shows up as latency rather than as a throughput ceiling.

Fory pays for that speed in bytes, but far less than the small payloads suggest. On the boxed long it is 33 % larger and on the flat bean 48 % larger, because its per-value framing is bigger and there is little else to amortise it against. As soon as the values carry real content, the gap closes: 20 % on the nested graph, 5 % on the map, and 4 % on the 1000-element list. Whether that matters depends on which resource you are short of. Bytes are network on every operation and memory on the server for the lifetime of the value; nanoseconds are CPU on the client. Most teams are shorter of one than the other, and they usually know which.

Switching is a one-line change, and it applies to every one of Redisson's distributed objects:

Config config = new Config();
config.setCodec(new ForyCodec());
config.useSingleServer().setAddress("redis://127.0.0.1:6379");

RedissonClient redisson = Redisson.create(config);

The usual rule applies: the same codec must write and read a given key. Bytes in Valkey and Redis carry no marker saying which codec produced them, so a mixed fleet reading the same keys with different codecs will get unreadable data. Roll the change out everywhere, or scope it to new keys.

Declaring Your Classes: The Lever Most People Skip

Both binary codecs accept a set of class names up front. It is usually presented as a security control — and it is one, since a codec that will only instantiate classes you have named cannot be talked into building something you did not expect. But it is also the only setting in this article that improves size, speed and safety at the same time.

Set<String> allowed = Set.of(
        "com.example.Order",
        "com.example.OrderLine",
        "java.util.ArrayList",
        "java.util.HashMap");

// Kryo needs every class it may meet, including the JDK containers
config.setCodec(new Kryo5Codec(allowed, false));

With the classes declared, both codecs replace a written-out class name with a numeric id. The effect on size is substantial, and lands almost entirely on Kryo:

PayloadKryo5 bytesKryo5 decodeFory bytesFory decode
Flat bean, 6 fields92 → 54 −41 %404 → 262 ns −35 %136 → 111 −18 %96 → 89 ns
Nested bean graph451 → 291 −35 %1 883 → 1 421 ns −25 %539 → 467 −13 %749 → 810 ns
HashMap, 100 beans6 274 → 6 119 −2 %17 668 → 16 481 ns6 600 → 6 5757 236 → 7 449 ns
ArrayList, 1000 strings24 025 → 24 00530 973 → 31 450 ns25 007 → 25 00717 870 → 20 431 ns

The pattern is straightforward once you see it: declaring classes removes class names from the bytes, so it helps in proportion to how much of the payload is class names. A small bean is mostly metadata and shrinks by 41 %. A list of a thousand strings contains one class name and a great deal of text, so it barely moves.

Kryo gets faster as well as smaller — a declared flat bean decodes in 262 ns against 404 — because there is less to read. Fory's timings do not move, which is worth knowing if speed is your only reason for considering the setting: declare classes for the size and the security, not for Fory's throughput. It also narrows the gap between the two codecs on small beans considerably, from 4.2× to 2.9×.

There is an operational cost, and it is worth being clear about it. Declaring classes means maintaining that list. A class the codec meets but has not been told about is a runtime failure rather than a slower path, which is exactly what you want from a security control and exactly what will page you at three in the morning if a deployment adds a type and misses the list.

JsonForyCodec vs JsonJackson3Codec

If another language has to read your data, or a human has to read it in redis-cli, binary is off the table. Redisson has two current JSON codecs: JsonJackson3Codec, built on Jackson 3, and the newer JsonForyCodec.

This turned out to be an unusually clean comparison, because for five of the six payloads the two codecs produce byte-identical output. Same format, same bytes, so speed is the only variable.

PayloadEncode Jackson 3Encode Fory JSONDecode Jackson 3Decode Fory JSONBytes
Boxed long266 ns116 ns273 ns132 ns16 = 16
String, 64 chars491 ns121 ns306 ns125 ns66 = 66
Flat bean, 6 fields813 ns420 ns861 ns566 ns162 = 162
Nested bean graph3 245 ns1 046 ns3 650 ns1 772 ns850 vs 572
ArrayList, 1000 strings104 352 ns32 352 ns50 825 ns53 009 ns27 025 = 27 025
HashMap, 100 beans53 413 ns47 482 ns76 641 ns66 367 ns17 261 = 17 261

JsonForyCodec wins almost everything, and by a wide margin on encode — writing the 1000-string list takes 32.4 µs against Jackson's 104.4 µs. Decode is a narrower but consistent win, with one exception: decoding that same list, where Jackson 3 is about 4 % faster. On the two large collections the gap closes to 1.1–1.2×, because at that size both are bound by the same UTF-8 and number parsing.

One Difference That Is Not About Speed

The nested bean graph is the one payload where the byte counts diverge — 572 against 850 — and the reason is a behavioural difference you need to know about before switching.

Jackson writes a @class property on every nested value. JsonForyCodec writes it only where the declared field type is not enough to reconstruct the value:

JsonFory   "address":{"street":"...","city":"...","zip":"42096","country":"US"}
Jackson 3  "address":{"@class":"com.example.Address","city":"...","country":"US",...}

That is where the 278 bytes go. It is also a real constraint: if a field declared as Address actually holds a subclass, JsonForyCodec restores it as a plain Address. The subclass and any fields it added are gone, and no error is raised. Jackson 3 restores the subclass correctly.

This is documented behaviour rather than a defect — Apache Fory intentionally supports no open polymorphism, and the codec adds type information only where it must. But it means JsonForyCodec is the right choice for values whose fields hold exactly their declared types, and the wrong one for values that rely on polymorphism. If you are not certain which describes your data, that uncertainty is itself the answer.

For more on what happens on the way back in, see our glossary entry on deserialization, and our guide to storing JSON in Valkey and Redis.

Compression: LZ4 vs Snappy vs ZStd

Redisson's three compression codecs — LZ4Codec, SnappyCodecV2 and ZStdCodec — are wrappers. Each one compresses the output of an inner codec, and each defaults to Kryo5Codec. We measured all three over exactly that, with plain Kryo5Codec as the baseline, because "which compressor" cannot answer "should I compress at all".

// any codec can be wrapped; the application code does not change
config.setCodec(new LZ4Codec(new Kryo5Codec()));

Compression buys two things at once: less bandwidth on every operation, which matters most in distributed caching, and more values resident before eviction starts discarding them. Four payloads join the set here, because compression ratio depends far more on the shape of the data than its size. A compressor measured only on random strings looks useless; one measured only on prose looks free.

PayloadUncompressedLZ4SnappyZStd
Flat bean, 6 fields9298 — 0.94×95 — 0.97×105 — 0.88×
Nested bean graph451398 — 1.13×398 — 1.13×361 — 1.25×
HashMap, 100 beans6 2744 788 — 1.31×4 772 — 1.31×3 469 — 1.81×
1000 strings, random24 02524 125 — 1.00×24 031 — 1.00×15 101 — 1.59×
1000 strings, 20-word vocabulary7 1382 994 — 2.38×2 160 — 3.30×1 519 — 4.70×
200 application log lines28 1847 395 — 3.81×7 308 — 3.86×4 289 — 6.57×
16 kB of prose16 392992 — 16.5×1 394 — 11.8×634 — 25.9×

ZStd compresses hardest on every payload, and its lead widens as redundancy rises. But two rows matter more than the ranking.

The flat bean gets bigger under all three. 92 bytes becomes 95 to 105. Compression has a header and a floor, and below a few hundred bytes it is a straight loss — you spend CPU to store more. Redisson does not size-gate this for you, so a cache of small values wrapped in a compression codec is paying twice for nothing. If your values are small, the answer to "which compressor" is "none".

Only ZStd compresses the random-string list at all. LZ4 and Snappy come out very slightly larger; ZStd gets 1.59×. The strings are random but drawn from a 26-letter alphabet, so they contain no repeated substrings for a matcher to find, but plenty of statistical redundancy — roughly 4.7 bits per byte. ZStd has entropy coding and captures that; LZ4 and Snappy have none. If your values are high-cardinality identifiers, hashes or base64, that is the row that describes your data.

Between LZ4 and Snappy there is no consistent winner on ratio, and which one leads depends entirely on the data. Snappy is better on short repetitive records; LZ4 is clearly better on long prose. That is not a decision you can make from someone else's table — it is a decision you make by running both against a sample of your own values.

What Compression Costs

Ratio is only half of the trade. Here is what each wrapper costs in time, as a multiple of storing the same value uncompressed:

PayloadLZ4 encodeSnappy encodeZStd encodeLZ4 decodeSnappy decodeZStd decode
Flat bean, 6 fields2.13×1.93×14.5×1.45×1.66×1.81×
Nested bean graph1.64×1.54×6.17×1.09×1.21×2.38×
HashMap, 100 beans1.79×1.80×3.78×1.10×1.28×1.67×
1000 strings, 20-word vocabulary1.71×1.65×2.93×1.25×1.26×1.62×
200 application log lines1.34×1.31×1.87×1.24×1.57×1.77×
16 kB of prose1.37×1.56×1.66×1.04×1.10×1.18×

LZ4 and Snappy are close to free. Encoding costs 1.3–2.1× the uncompressed path and decoding 1.0–1.7×. Against data that compresses 2–16×, that is an easy trade in almost any system where bytes cost money.

ZStd's cost depends on the size of the value, sharply. On the 92-byte bean it is 14.5×, because compressing a value that small is almost entirely fixed overhead — and it produces a larger result anyway, so nothing about that row is worth having. As values grow the overhead amortises: 3.8× on the map, 1.9× on the log lines, 1.7× on the prose. At the sizes where you would actually turn compression on, ZStd costs roughly 30–60 % more CPU than LZ4 and returns 40–70 % fewer bytes.

That makes the choice a straightforward one about which resource is scarce. If CPU on the client is the constraint, LZ4. If bandwidth or server memory is the constraint — and for most people running a managed Valkey or Redis instance, it is — ZStd on large values is the better deal, and the one worth measuring against your own data.

What JSON Costs Against Binary

Because every codec here ran in the same harness against the same payloads, the binary and JSON results can be read directly against each other. Decoding the HashMap of 100 beans:

CodecFormatDecodeBytes
ForyCodecbinary7 236 ns6 600
Kryo5Codecbinary17 668 ns6 274
JsonForyCodecJSON66 367 ns17 261
JsonJackson3CodecJSON76 641 ns17 261

The fastest JSON codec is roughly nine times slower than the fastest binary one and stores 2.6× the bytes. That is the price of JSON, and it is worth paying when something that is not a JVM has to read the data, or when being able to inspect a value by eye is genuinely valuable during an incident.

It is not worth paying for readability alone in a system where every producer and consumer is Java. If that is why JSON is in place, this table is the argument for revisiting it.

Allocation: The Axis Nobody Reports

Time and bytes are the two numbers every serialization benchmark publishes. The third — how much garbage a codec produces per operation — is the one that turns up later as GC pause time in a service that looked fine in a microbenchmark. JMH measures it directly.

CodecEncode, nested beanEncode, 1000 stringsDecode, nested beanDecode, 1000 strings
Kryo5Codec80 B80 B2 208 B68 184 B
ForyCodec136 B136 B2 072 B68 176 B
JsonForyCodec878 B27 024 B2 434 B87 264 B
JsonJackson3Codec1 288 B473 B3 304 B79 936 B

Both binary codecs are effectively allocation-free on encode: 80 and 136 bytes to produce a 25 kB value, because their working buffers are pooled and reused. Whatever else you weigh, neither one is a source of garbage.

The JSON row is more interesting, and it is the one place in this article where the faster codec is not simply the better one. JsonForyCodec encodes the 1000-string list three times faster than Jackson, but allocates 27 kB doing it against Jackson's 473 B — Jackson recycles its buffers through a thread-local, and Fory's JSON path does not. In a tight benchmark loop the young generation absorbs that cheaply and it barely shows in the timing. In a service writing thousands of large values a second it is real GC pressure, and it will surface somewhere other than the codec.

Decode allocation is dominated by the objects being built rather than by the codec, which is why the four rows are much closer: a thousand strings have to exist regardless of who parsed them. The differences that remain — Fory marginally under Kryo, both JSON codecs above both binary ones — follow the same ordering as the size table, for the same reason.

If you are choosing between the two JSON codecs and your values are large and write-heavy, this is the table to weigh, not the timing one.

Choosing a Codec

The numbers point at a small number of clear answers. Codec choice is one of a handful of client defaults worth reviewing deliberately rather than inheriting.

If this describes youUseBecause
JVM-only, latency matters, values of any real sizeForyCodecFaster on every payload, 1.7–4.2×, for about 4 % more bytes
JVM-only, values are small, memory or bandwidth is the constraintKryo5Codec with declared classesA declared flat bean is 54 bytes against Fory's 111
Another language reads the dataJsonForyCodecSame bytes as Jackson, 1.1–4.1× faster to encode — unless fields hold subtypes, or values are large and write-heavy
Another language reads it, and your model uses polymorphismJsonJackson3CodecRestores subclasses correctly; silent type loss costs more than 2× on a benchmark
Values are large and repetitive, CPU is the constraintWrap in LZ4Codec3.8× on log lines, 16.5× on prose, for 1.3–1.8× the encode cost
Values are large and bandwidth or memory is the constraintWrap in ZStdCodec40–70 % fewer bytes than LZ4 for 30–60 % more CPU; the only one that compresses high-entropy data at all
Values are under a few hundred bytesNo compressionAll three compressors make small values bigger
Data crosses a trust boundaryDeclared classes, alwaysSee the codec guide on insecure deserialization

Two things this benchmark deliberately does not tell you. It does not cover the security dimension, which the codec selection guide handles properly and which should outrank any number here when the two conflict. And it is single-threaded, so it says nothing about how these codecs behave under concurrent load — they use different mechanisms to achieve thread safety, and that is a separate question we have not measured.

There is also a codec choice that is faster than all of them, which is not calling one. Reads served from a client-side cache never touch the network or a codec at all. Where a working set is small enough to keep locally, the near cache in Redisson PRO removes the entire cost this article measures. Codec choice matters most for what is left over.