Redisson Codec Benchmark: What Kryo5, Fory, JSON and Compression Actually Cost
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.
| Payload | Encode Kryo5 | Encode Fory | Decode Kryo5 | Decode Fory | Bytes Kryo5 | Bytes Fory |
|---|---|---|---|---|---|---|
Boxed long | 232 ns | 138 ns | 172 ns | 52 ns | 9 | 12 |
| String, 64 chars | 306 ns | 152 ns | 214 ns | 80 ns | 67 | 69 |
| Flat bean, 6 fields | 346 ns | 182 ns | 404 ns | 96 ns | 92 | 136 |
| Nested bean graph | 1 423 ns | 429 ns | 1 883 ns | 749 ns | 451 | 539 |
ArrayList, 1000 strings | 21 506 ns | 11 509 ns | 30 973 ns | 17 870 ns | 24 025 | 25 007 |
HashMap, 100 beans | 13 799 ns | 4 353 ns | 17 668 ns | 7 236 ns | 6 274 | 6 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:
| Payload | Kryo5 bytes | Kryo5 decode | Fory bytes | Fory decode |
|---|---|---|---|---|
| Flat bean, 6 fields | 92 → 54 −41 % | 404 → 262 ns −35 % | 136 → 111 −18 % | 96 → 89 ns |
| Nested bean graph | 451 → 291 −35 % | 1 883 → 1 421 ns −25 % | 539 → 467 −13 % | 749 → 810 ns |
HashMap, 100 beans | 6 274 → 6 119 −2 % | 17 668 → 16 481 ns | 6 600 → 6 575 | 7 236 → 7 449 ns |
ArrayList, 1000 strings | 24 025 → 24 005 | 30 973 → 31 450 ns | 25 007 → 25 007 | 17 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.
| Payload | Encode Jackson 3 | Encode Fory JSON | Decode Jackson 3 | Decode Fory JSON | Bytes |
|---|---|---|---|---|---|
Boxed long | 266 ns | 116 ns | 273 ns | 132 ns | 16 = 16 |
| String, 64 chars | 491 ns | 121 ns | 306 ns | 125 ns | 66 = 66 |
| Flat bean, 6 fields | 813 ns | 420 ns | 861 ns | 566 ns | 162 = 162 |
| Nested bean graph | 3 245 ns | 1 046 ns | 3 650 ns | 1 772 ns | 850 vs 572 |
ArrayList, 1000 strings | 104 352 ns | 32 352 ns | 50 825 ns | 53 009 ns | 27 025 = 27 025 |
HashMap, 100 beans | 53 413 ns | 47 482 ns | 76 641 ns | 66 367 ns | 17 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.
| Payload | Uncompressed | LZ4 | Snappy | ZStd |
|---|---|---|---|---|
| Flat bean, 6 fields | 92 | 98 — 0.94× | 95 — 0.97× | 105 — 0.88× |
| Nested bean graph | 451 | 398 — 1.13× | 398 — 1.13× | 361 — 1.25× |
HashMap, 100 beans | 6 274 | 4 788 — 1.31× | 4 772 — 1.31× | 3 469 — 1.81× |
| 1000 strings, random | 24 025 | 24 125 — 1.00× | 24 031 — 1.00× | 15 101 — 1.59× |
| 1000 strings, 20-word vocabulary | 7 138 | 2 994 — 2.38× | 2 160 — 3.30× | 1 519 — 4.70× |
| 200 application log lines | 28 184 | 7 395 — 3.81× | 7 308 — 3.86× | 4 289 — 6.57× |
| 16 kB of prose | 16 392 | 992 — 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:
| Payload | LZ4 encode | Snappy encode | ZStd encode | LZ4 decode | Snappy decode | ZStd decode |
|---|---|---|---|---|---|---|
| Flat bean, 6 fields | 2.13× | 1.93× | 14.5× | 1.45× | 1.66× | 1.81× |
| Nested bean graph | 1.64× | 1.54× | 6.17× | 1.09× | 1.21× | 2.38× |
HashMap, 100 beans | 1.79× | 1.80× | 3.78× | 1.10× | 1.28× | 1.67× |
| 1000 strings, 20-word vocabulary | 1.71× | 1.65× | 2.93× | 1.25× | 1.26× | 1.62× |
| 200 application log lines | 1.34× | 1.31× | 1.87× | 1.24× | 1.57× | 1.77× |
| 16 kB of prose | 1.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:
| Codec | Format | Decode | Bytes |
|---|---|---|---|
ForyCodec | binary | 7 236 ns | 6 600 |
Kryo5Codec | binary | 17 668 ns | 6 274 |
JsonForyCodec | JSON | 66 367 ns | 17 261 |
JsonJackson3Codec | JSON | 76 641 ns | 17 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.
| Codec | Encode, nested bean | Encode, 1000 strings | Decode, nested bean | Decode, 1000 strings |
|---|---|---|---|---|
Kryo5Codec | 80 B | 80 B | 2 208 B | 68 184 B |
ForyCodec | 136 B | 136 B | 2 072 B | 68 176 B |
JsonForyCodec | 878 B | 27 024 B | 2 434 B | 87 264 B |
JsonJackson3Codec | 1 288 B | 473 B | 3 304 B | 79 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 you | Use | Because |
|---|---|---|
| JVM-only, latency matters, values of any real size | ForyCodec | Faster on every payload, 1.7–4.2×, for about 4 % more bytes |
| JVM-only, values are small, memory or bandwidth is the constraint | Kryo5Codec with declared classes | A declared flat bean is 54 bytes against Fory's 111 |
| Another language reads the data | JsonForyCodec | Same 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 polymorphism | JsonJackson3Codec | Restores subclasses correctly; silent type loss costs more than 2× on a benchmark |
| Values are large and repetitive, CPU is the constraint | Wrap in LZ4Codec | 3.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 constraint | Wrap in ZStdCodec | 40–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 bytes | No compression | All three compressors make small values bigger |
| Data crosses a trust boundary | Declared classes, always | See 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.