# rkyv: zero-copy deserialization in Rust, and what it costs to use

> rkyv archives Rust types into a byte buffer you can read in place, without a parsing pass. It is a good fit for memory-mapped data and hot paths, and a poor fit when the bytes come from an untrusted peer.

**rkyv/rkyv** — Zero-copy deserialization framework for Rust

- Repository: https://github.com/rkyv/rkyv
- Website: https://rkyv.org
- Stars: 4,365 · Forks: 234
- Language: Rust
- License: MIT
- Published: 2026-09-23 · Updated: 2026-09-23 · Language: en
- Canonical page: https://hysenlabs.com/projects/rkyv-rkyv

## The problem rkyv solves: reading data without parsing it first

Most serialization libraries make you pay twice. You write a struct out, and later you read the bytes back and reconstruct the struct field by field. The reconstruction is where the time goes, and for large collections it is where the allocations go too.

rkyv (the README glosses the name as archive) takes a different route. It writes your value into a byte buffer in a layout that already looks like the in-memory representation, so reading it back can mean casting a pointer and handing out a reference. The README's own framing is that serializing "is as easy as a single function call" and that access gives you "fast zero-copy deserialization".

The audience is narrow by design. This is for Rust programs where the same process, or a cooperating process, writes and reads the data, and where the read path is hot enough that a parsing pass shows up. Memory-mapped files, shared-memory buffers, and caches that survive a restart are the obvious cases. If you are producing JSON for a browser, rkyv's layout buys you nothing.

## How the archive layout works, and why validation exists

The derive macro is the entry point. When you derive Archive on a struct, rkyv generates a second type, conventionally named ArchivedTest, whose fields hold the same data in a form that can live in the buffer. Relative pointers are stored instead of absolute ones, because the buffer can be moved or mapped at a different address.

The repository layout explains the split. The rkyv crate holds the core library, rkyv_derive holds the proc macro that generates the archived types, and the workspace pins four supporting crates: bytecheck for validation, rend for endian-agnostic handling, ptr_meta for pointer manipulation, and rancor for error handling. The Cargo.toml lists all four under workspace dependencies and patches them to their Git repositories, so the published crate and the workspace build are not pulling from identical sources.

Validation is the interesting design decision. Because an archived value contains pointers, a corrupted or hostile buffer can point anywhere. rkyv::access runs a check before handing out a reference, and the README notes that "the safe API requires the bytecheck feature (enabled by default)". The unchecked path skips that work entirely, which is where the performance claim comes from and also where the safety argument ends. rkyv_dyn, which adds trait object support, is present in the repository but commented out of the workspace members list in Cargo.toml, so it is not part of the default build.

## Installing rkyv and archiving your first struct

The core crate is published on crates.io, and the README links to docs.rs for the API. Add it to a project with cargo add, then derive the three traits the README's example uses: Archive, Serialize, and Deserialize.

```bash
cargo add rkyv
```

The derive accepts attributes. The README's example uses compare(PartialEq) to generate a PartialEq implementation between the original and archived types, and derive(Debug) to pass a derive through to the generated archived type. That second one is worth copying while you are learning the API, since it makes the archived value printable.

```rust
use rkyv::{Archive, Deserialize, Serialize};

#[derive(Archive, Deserialize, Serialize, Debug, PartialEq)]
#[rkyv(compare(PartialEq), derive(Debug))]
struct Test {
    int: u8,
    string: String,
    option: Option<Vec<i32>>,
}
```

Serializing a value and reading it back takes two calls. The README shows rkyv::to_bytes::<Error>(&value), with Error coming from the rancor crate, and then rkyv::access::<ArchivedTest, Error>(&bytes[..]) to get a reference into the buffer. The README's example asserts that the archived value equals the original, which works because of the compare attribute above.

```rust
use rkyv::{access, deserialize, rancor::Error};

let bytes = rkyv::to_bytes::<Error>(&value).unwrap();
let archived = access::<ArchivedTest, Error>(&bytes[..]).unwrap();
let deserialized = deserialize::<Test, Error>(archived).unwrap();
```

If you want to control allocation, the README points at rkyv::api::high::to_bytes_with_alloc together with rkyv::ser::allocator::Arena, which lets you supply the scratch space yourself rather than using the default. That is the version to reach for when you are archiving in a loop and want to reuse a buffer.

## The unsafe path is not an optimization detail, it is the whole trade-off

rkyv::access_unchecked is the function to think hardest about. The README presents it as the way to get "maximum performance", and it is, because it does no checking at all. The consequence is that the caller is asserting the buffer is well-formed. If it is not, the code is reading through pointers that were never validated.

This is not a bug in rkyv. It is the shape of zero-copy deserialization: you are trading a parsing pass for a validation pass, and you can only drop the validation pass if you trust the bytes. Data you wrote yourself and have not let leave your control is the case where that holds. Data from a network peer, a user-uploaded file, or a cache that another process can write to is not.

The safe path has its own cost, and it is worth being honest about it. Validation walks the structure and checks the pointers, so on small payloads the check can dominate the read. rkyv wins on large, pointer-heavy structures where the alternative is a full parse plus allocation. On a struct with three fields, the difference between rkyv and a conventional format may not be measurable in your application, and you have taken on a derive macro, a generated type, and a feature flag for nothing.

## rkyv against bincode, and when a conventional format is the right answer

bincode is the comparison people reach for, and the difference is structural rather than a matter of tuning. bincode encodes a value into a compact byte stream and decodes it back into an owned value. The bytes on disk are not the value in memory. That means deserialization allocates, and for a Vec or a String it allocates every time.

rkyv skips that step. The archived form is the buffer, so reading a field is a pointer offset rather than a decode. The cost moves to archive time, where rkyv does more work up front, and to the validation pass if you use the safe API. If your workload reads the same data many times and writes it rarely, the trade is favourable. If you write constantly and read once, it is not.

A second alternative is to keep the conventional format and accept the parse. That is the correct call when the data crosses a trust boundary, when the reader is not a Rust program, or when the schema changes often enough that you want a format with a documented wire representation. rkyv's layout is an implementation detail of the crate, and the README does not document a versioning or migration story for archived data. Treat an archive as tied to the schema that produced it.

## Version, licence, and what an upgrade actually involves

The workspace Cargo.toml sets version to 0.8.18 and rust-version to 1.81, so the toolchain floor is explicit and checkable before you start. The licence is MIT, declared both in the Cargo.toml and in the LICENSE file at the repository root. MIT is permissive and imposes no copyleft obligation on your code, but the usual caveat applies: this is a description of what the repository states, not legal advice, and the rkyv_derive crate that generates code into your build is a separate package you should account for in a licence audit.

Upgrade cost is where rkyv asks for attention. The generated archived types are part of your public API surface if you expose them, and the derive attributes are part of your source. A change in the derive macro can change the generated type, which means a version bump can ripple into code that names ArchivedTest directly. The Cargo.toml also patches bytecheck, ptr_meta, rancor, and rend to their Git repositories, which tells you these crates move together. Pinning rkyv and its supporting crates as a set is safer than upgrading one at a time.

The repository was last pushed on 2026-09-09, so it is not dormant, but the README does not document a deprecation policy, a minimum supported Rust version schedule beyond the rust-version field, or a rollback path for archived data written by an older version.

## Conclusion

Adopt rkyv when you control both ends of the data and the read path is hot enough that a parsing pass matters, for example memory-mapped files or inter-process buffers you wrote yourself. Do not adopt it as a drop-in replacement for a general-purpose format on data from untrusted peers, where the safe access path's validation pass works against the reason you came. Before committing, check the rust-version field in Cargo.toml against your toolchain, confirm the bytecheck feature is on if you want rkyv::access, and decide which of access and access_unchecked your code will use, because that choice determines whether you can accept bytes you did not produce.

## FAQ

### What is rkyv used for in Rust?

It archives Rust values into a byte buffer whose layout matches the in-memory representation, so reading them back can be a pointer cast rather than a parse. The README describes it as a zero-copy deserialization framework and points to the rkyv book for the motivation and architecture.

### Do I need the bytecheck feature to use rkyv safely?

Yes. The README states that the safe API requires the bytecheck feature and that it is enabled by default. Without it, rkyv::access is not the path you are on.

### How do I install rkyv and archive a struct?

Add the crate with cargo add rkyv, derive Archive, Serialize, and Deserialize on your type, then call rkyv::to_bytes::<Error>(&value) and rkyv::access::<ArchivedYourType, Error>(&bytes[..]). The README's example uses the rancor Error type and a compare(PartialEq) attribute to make the archived value comparable to the original.

### What is the difference between rkyv::access and rkyv::access_unchecked?

access runs validation before returning a reference into the buffer, while access_unchecked does no checking and the README presents it as the maximum-performance option. The unchecked call is only appropriate when you know the bytes are well-formed.

## Sources

- [License: MIT](https://github.com/rkyv/rkyv/blob/main/LICENSE)
- [Project website](https://rkyv.org)
- [README](https://github.com/rkyv/rkyv/blob/main/README.md)
- [Releases](https://github.com/rkyv/rkyv/releases)
- [rkyv/rkyv on GitHub](https://github.com/rkyv/rkyv)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/rkyv-rkyv
