Open-source project
PSeitz/lz4_flex avatar
PSeitz/lz4_flex

lz4_flex: an LZ4 codec in Rust that is safe by default

Fastest pure Rust implementation of LZ4 compression/decompression.

619 stars64 forksRustMIT

At a glance

What is it?
lz4_flex is a pure Rust implementation of the LZ4 block and frame formats, written so that no unsafe code runs unless you ask for it, with feature flags that trade safety for speed. It fits a Rust service that needs fast compression without a C dependency, and the trade you accept is that the frame format is unavailable in a no_std build.
Who is it for?
lz4_flex fits a Rust project that wants LZ4 without linking the C library, especially one that has a no_std target for block data or a policy against unsafe code by default. It is the wrong choice if you need the frame format in a no_std build, if you want the highest compression ratio rather than speed, or if your pipeline already speaks Snappy or Zstd more cheaply.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 73 days ago.
What is it written in?
Mainly Rust, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What the crate does and where it came from

lz4_flex is described in the README as the fastest LZ4 implementation in Rust, and the project description calls it the fastest pure Rust implementation of LZ4 compression and decompression. The Cargo.toml description is more measured: a LZ4 implementation in Rust with no unsafe by default.

The origin is worth knowing because it explains the shape of the code. The README says it was originally based on the redox-os lz4 compression crate, but is now a complete rewrite. The difference matters for a codec: a rewrite is how you get the safety properties and the buffer handling that the original design did not have.

Two formats are supported, the LZ4 block format and the LZ4 frame format, the latter credited to another contributor. The block format is the raw compressed block, and the frame format is the container that wraps multiple blocks with framing metadata, which is what you normally want on disk or on the wire.

The other features on the list are less obvious from the name. There is 32-bit support, so the crate builds for embedded and legacy targets, not just x86_64. There is no-std support with the block format, contributed by another user. There is a feature flag system to configure safe and unsafe code usage, which is the subject of the next section. And there is a 1.5 second clean release build time, which sounds trivial until you have waited on a codec crate that pulls in a large dependency tree.

The audience is a Rust developer who needs LZ4 and would rather not have a C dependency in the tree. The licence is MIT with a LICENSE file at the root, the last push was on 2026-08-11 with the repository not archived, and the release history shows 0.12.0 in November 2025, 0.13.0 in March 2026 and 0.13.1 in May 2026, with the manifest already at 0.14.0.

Feature flags decide whether unsafe code runs at all

This is the design decision that separates lz4_flex from most codecs written in Rust, and it is worth understanding before you benchmark anything.

By default, compression and decompression use no unsafe code, through the default feature flags safe-encode and safe-decode. The Cargo.toml lists the full feature set, and the defaults are std, safe-encode, safe-decode, frame and checked-decode. The safe features are empty feature lists, which means they are pure markers, and the real behaviour is the absence of the unsafe paths.

If you want the extra speed, you disable the defaults:

toml
lz4_flex = { version = "0.12", default-features = false, features = ["alloc"] }

The plain dependency, with the safe defaults on, is:

toml
lz4_flex = { version = "0.12" }

There is a third flag worth reading the comment on. checked-decode is described in the manifest as adding important checks while decoding, with the instruction to only remove it on trusted input. That is a security-relevant decision encoded as a Cargo feature, and it is unusual to see it spelled out that plainly. If you decode data from outside your trust boundary, that flag should stay on, because it is what stops a malformed input from causing undefined behaviour in the unsafe decoder.

The frame feature pulls in std and the twox-hash dependency, which is how the frame format gets its content checksum. The alloc feature is the allocator, and std implies alloc.

So the choice an adopter makes is three flags: safe-encode, safe-decode and checked-decode. The README's own benchmark table shows the cost of turning them off, and it is not free. On the 66 KB JSON sample the safe configuration compresses at 1272 MiB/s and decompresses at 4540 MiB/s, while the unsafe configuration with unchecked decoding reaches 1615 MiB/s and 5973 MiB/s. The ratio is identical at 0.2284 in all three rows.

Block format is in-memory only, frame format is what you store

The README is explicit about the split between the two formats, and the reason is a memory model difference rather than a compression difference.

The block format is only valid for smaller data chunks, as the block is de-compressed in memory. For larger data you use the frame format, which consists of multiple blocks. So if you have a buffer that fits in memory, the block format with a size prefix is the simplest thing to use, and the code for it is short:

rust
use lz4_flex::block::{compress_prepend_size, decompress_size_prepended};

fn main(){
    let input: &[u8] = b"Hello people, what's up?";
    let compressed = compress_prepend_size(input);
    let uncompressed = decompress_size_prepended(&compressed).unwrap();
    assert_eq!(input, uncompressed);
}

The two function names carry the two important details. compress_prepend_size writes the uncompressed length ahead of the data so that decompression knows how much to expect, and decompress_size_prepended reads that length back. That is the whole reason the image format variant needs no side channel for its size, and it is what makes the round trip in the example a single call each way.

The frame format exists to avoid holding everything in memory at once. The frame is a sequence of blocks with a header and per-block metadata, and the crate can compress into and out of a writer, which is what a streaming pipeline needs.

The README also notes that frame format uses std::io::Write, which is not available in core, and that no_std support is therefore currently only for the block format. That is a hard constraint, not a caveat: a no_std target that needs framed data cannot use this crate for it.

The bindings section points to wrappers in other languages, with a Node.js binding, a WebAssembly binding and a Python binding, all separate projects. That is a useful signal about the audience, since it means the crate is expected to be consumed from other ecosystems too.

no_std with a caller-supplied table, and the Miri and fuzz setup

The no_std story has two parts, and the second one is unusual.

The first part is the feature flags. For environments without an allocator, you additionally disable the alloc feature, which leaves the crate with nothing but core. The second part is the API shape. The _into variants, compress_into and decompress_into and their relatives, operate on slices you provide rather than allocating internally, and the compression hash table is either placed on the stack at 8 to 16 KB depending on input size, or provided by the caller via compress_into_with_table.

Handing the caller the hash table is the interesting decision. It means a memory-constrained embedded target can keep that buffer in a fixed allocation it already owns, and reuse it, rather than the library having to manage it. For a no_std target that is a meaningful amount of control over peak memory.

The verification tooling is where the project's seriousness shows. Miri, the Rust interpreter for detecting undefined behaviour, is used to find issues related to incorrect unsafe usage, and the README gives the exact invocation, with isolation and stacked borrows disabled and the frame feature enabled:

The invocation disables isolation and stacked borrows in Miri and runs the miri test target with only the frame feature enabled.

Then there is a fuzz directory with several targets, each asserting a specific property. Two generate corrupted data for the decompressor, and one asserts that the decompressor does not leak previous data when reusing an output buffer, which is the classic bug in a decoder that reuses a buffer. Two more assert that a compression and decompression roundtrip returns the original input, one for the block format and one for the frame format, and a final one asserts that compression with the C implementation and decompression with lz4_flex returns the original input, which checks interoperability with the reference implementation rather than only self-consistency.

The examples directory holds four small programs, compress, compress_block, decompress and decompress_block, and the tests directory sits alongside miri_tests. The repository also carries a SECURITY.md and a CHANGELOG.md, with cliff.toml for generating the changelog from commits.

The benchmark table and what it does and does not tell you

The README carries a comparison table, and it is worth reading with the caveats the README itself provides rather than as a claim.

The stated conditions are an AMD Ryzen 7 5900HX with rustc 1.69.0 on Manjaro, CPU Boost disabled and the CPU governor set to Performance, and the README says the results come from a benchmark in this project using the block format, on a 66 KB JSON sample and a 10 MB Dickens text. A second table repeats the comparison with the benchmark suite details, and notes the test files live in the benches folder and the benchmark is run with criterion.

The comparisons are against lz-fear, the C implementation through Rust bindings via the lzzzz crate, and snappy, so the reference point is the C library rather than a hypothetical.

On the JSON sample, the safe lz4_flex configuration compresses at 1272 MiB/s and decompresses at 4540 MiB/s, the unsafe configuration reaches 1615 MiB/s and 5512 MiB/s, the C binding is at 1469 MiB/s and 5313 MiB/s, and snappy is at 1452 MiB/s and 1649 MiB/s.

On the larger Dickens sample the picture changes, and that is the point worth taking away. The safe configuration drops to 259 MiB/s compression and 2338 MiB/s decompression, while the C binding is at 324 MiB/s and 2759 MiB/s, so the Rust implementation is behind on both there. The unsafe configuration matches the C binding at 347 MiB/s compression and trails it on decompression at 2734 MiB/s.

So the headline claim holds on small, compressible JSON and does not hold on a larger text corpus in the safe configuration. Whether that matters depends on whether your data is cache-friendly or stream-sized, and the compression ratio is essentially identical across the implementations, at 0.2284 on the JSON and 0.6372 on Dickens, so the choice is not about how well it compresses.

Where lz4_flex is the wrong tool

The first wrong-tool case is a no_std target that needs the frame format. The README says it directly, that no_std support is currently only for the block format, because the frame format uses std::io::Write, which is not available in core. If your embedded target stores framed data, this crate cannot do it.

The second is a requirement for compression ratio. The TODO list in the README has one item, high compression, and the benchmark tables show the ratios are around 0.23 and 0.64 depending on the data. If you need to squeeze more bytes out of the payload, you want a different algorithm, and the comparison set in the README does not include one. Snappy, which is in the table, trades some ratio for speed in the other direction.

The third is a policy that forbids unsafe code entirely, combined with a need for top throughput. The default configuration is safe, and the safe configuration is measurably behind the C binding on the larger sample. Turning the flags off is exactly what the design invites, so a project whose security policy forbids unsafe code is choosing the slower path knowingly, and should confirm the numbers are acceptable before committing.

The fourth is an application that already has a compression format in place. LZ4 is a fast codec, not a good general-purpose choice for long-term storage of compressible data, and mixing codecs in a data pipeline tends to produce the situation where one link expects one format and another writes another. The crate will happily round-trip its own output, but interoperability with other implementations is a property you verify with the fuzz targets rather than assume.

The fifth is an expectation of a maintained upstream. The project is a single-crate library with a small dependency footprint and one optional dependency, which is a low maintenance burden, but the releases are infrequent and the manifest version is ahead of the last published one. The last push was on 2026-08-11, so the code is not frozen, but the release cadence is not one you should build a deployment schedule around.

Editorial conclusion

lz4_flex fits a Rust project that wants LZ4 without linking the C library, especially one that has a no_std target for block data or a policy against unsafe code by default. It is the wrong choice if you need the frame format in a no_std build, if you want the highest compression ratio rather than speed, or if your pipeline already speaks Snappy or Zstd more cheaply. Before adopting it, decide whether the safe-encode and safe-decode defaults are what you want for your data, since turning them off is what buys the extra speed, and run the fuzz roundtrip target against your own compressed payloads before you trust the decoder on untrusted input.

Frequently asked questions

Does lz4_flex use unsafe code?

Not by default, since the safe-encode and safe-decode feature flags are on by default, and the Cargo.toml description calls it a LZ4 implementation in Rust with no unsafe by default, though disabling the default features enables the faster unsafe paths.

Which LZ4 formats does lz4_flex support?

It supports the LZ4 block format and the LZ4 frame format, and the README notes that the block format is only valid for smaller data chunks since it is de-compressed in memory, while the frame format consists of multiple blocks for larger data.

Can lz4_flex be used in a no_std environment?

Yes, for the block format only, since the frame format uses std::io::Write which is not available in core, and for environments without an allocator you additionally disable the alloc feature and use the _into variants with a caller-provided hash table.

How does the lz4_flex project verify its decompressor?

With Miri for incorrect unsafe usage and with fuzz targets that generate corrupted data, assert the decompressor does not leak previous data when reusing an output buffer, check that roundtrips return the original input, and check interoperability with the C implementation.

Official sources

  1. Issues
  2. License: MIT
  3. PSeitz/lz4_flex on GitHub
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/pseitz-lz4-flex.svg)](https://hysenlabs.com/projects/pseitz-lz4-flex)

Was this useful?