High-performance, clone-efficient byte buffers optimized for small data.
Introduction
smol-bytes provides byte buffers that store up to 62 bytes inline — no heap
allocation, and cloning an inline value is a plain 64-byte copy. Larger values
use bytes-backed heap storage with
reference-counted clones. Two strategies control what happens when a heap
value shrinks back under the inline threshold, and UTF-8 wrappers layer
String-like, boundary-checked APIs over the same storage.
This is a good fit when most values are small and cloned often — tokens in a lexer, keys and field names, protocol headers — and allocation pressure matters.
Install
[]
= "0.1"
Python (pip install smol-bytes) and JavaScript (npm install smol-bytes)
packages are also published; build-from-source instructions for each binding
are below.
Quick start
The root Bytes type is an alias for shared::Bytes. Ordinary copies and
static values of at most 62 bytes start inline; imported or owner-backed
values and retained shared heap views can remain heap-backed below that
threshold.
use Bytes;
let small = from_static;
assert!;
let cloned = small.clone; // 64-byte copy, no allocation
assert_eq!;
let heap = copy_from_slice;
assert!;
assert_eq!;
Storage strategies
shared::Bytes preserves heap storage once a value lives there, keeping
conversions with bytes::Bytes zero-copy. compact::Bytes copies a shrinking
view of 62 bytes or fewer back into inline storage to release the allocation.
use ;
let mut shared = from;
let mut compact = from;
shared.advance;
compact.advance;
assert_eq!;
assert_eq!;
assert!; // stays shareable and zero-copy convertible
assert!; // allocation released, contents inlined
Rule of thumb: use shared (the default) for I/O and bytes interop; use
compact when memory footprint matters more than conversion speed.
Types
| Type | Storage | Mutable | Purpose |
|---|---|---|---|
Buffer |
Fixed inline bytes, up to 62 bytes | Yes | no_std fixed buffer |
Bytes / shared::Bytes |
Inline or shared heap-backed bytes | No | Immutable shared view |
compact::Bytes |
Inline or compacting heap-backed bytes | No | Immutable compacting view |
BytesMut |
Inline, then growable bytes::BytesMut storage |
Yes | Mutable byte buffer |
Utf8Buffer |
Fixed inline, valid UTF-8 bytes | Yes | Small mutable UTF-8 value |
Utf8Bytes / compact::Utf8Bytes |
Shared or compacting UTF-8 bytes | No | Immutable UTF-8 value |
Utf8BytesMut |
Inline or growable valid UTF-8 bytes | Yes | Mutable UTF-8 value |
Every handle is 64 bytes. All byte types implement bytes::Buf, and the
mutable ones implement bytes::BufMut.
BytesMut::split_to and BytesMut::split_off return Ok(BytesMut) when the
output is growable heap storage and Err(Buffer) when the output is fixed
inline storage. The try_split_* variants add an outer bounds Result, so
their shape is Result<Result<BytesMut, Buffer>, OutOfBounds>.
Rust UTF-8 split and slice indices are byte offsets that must fall on
character boundaries; offenders panic, and the try_split_to,
try_split_off, and try_slice variants return errors instead. (The Python
bindings differ deliberately — see below.)
bytes interop
Conversions with the bytes crate are zero-copy wherever the representation
allows:
bytes::Bytes -> shared::Bytesshares the allocation (Fromimpl).shared::Bytes -> bytes::Bytesreuses heap backing; inline values copy.compact::Bytes::from(bytes::Bytes)inlines payloads of at most 62 bytes and shares larger ones.BytesMut::freeze_shared/freeze_compactconvert without copying heap contents;Bytes::try_into_mutreclaims unique heap allocations.
Features and MSRV
| Feature | Description |
|---|---|
std (default) |
Standard-library support and the heap-backed types |
alloc |
Heap-backed types without std |
serde |
Serde support |
borsh |
Borsh support |
arbitrary |
arbitrary support for generated values |
quickcheck |
QuickCheck support |
async-graphql |
Bytes and String GraphQL scalars for Bytes and Utf8Bytes; implies std |
sqlx |
sqlx Type/Encode/Decode for Bytes and Utf8Bytes; implies std |
pyo3 |
Python bindings; implies std |
wasm |
WebAssembly bindings; implies std |
With no features enabled the crate is no_std and provides the fixed
Buffer and Utf8Buffer types; alloc adds the heap-backed types without
std.
The sqlx bindings decode by borrowing from the row, so a value of at most 62
bytes is built inline with no heap allocation at all. That costs one thing:
PostgreSQL will not lend BYTEA out in a simple (unprepared) query, so code on
that path — raw_sql, or a SQL string handed straight to an Executor — has
to decode the byte types as Vec<u8> and convert. query, query_as and the
query! macros carry an argument list and are therefore prepared, so they are
unaffected, as are the UTF-8 types and every MySQL and SQLite path.
Rust 1.85 is the library MSRV, and the bytes dependency floor is 1.10.
Development-only test and benchmark dependencies can require a newer
compiler, and so do both optional integrations, to different floors:
async-graphql 7.2 declares Rust 1.89, and 7.2 is the floor because the 7.0
releases do not build against the 7.2 derive crate their own dependency range
admits; sqlx 0.9 declares Rust 1.94, and 0.9 is the floor because the impls are
written against the lifetime-free Database::ArgumentBuffer introduced there.
Enabling either raises the MSRV for the whole build.
Verification
The test and CI story is deliberately heavier than the crate's size:
- Unit, integration, doc, and property tests (proptest state-machine
comparisons against
Vec/String, plusquickcheckandarbitrarygenerators that preserve type invariants). - Miri over the full suite under both stacked borrows and tree borrows with strict provenance and symbolic alignment checks.
- Address, leak, memory, and thread sanitizers in CI.
- Deserialization is hardened: borsh reads length-prefixed payloads in bounded chunks instead of trusting the length prefix, and serde sequence hints are capped before preallocating.
Python
Install the published package with pip install smol-bytes (Python 3.11+).
To build from a checkout instead, Python 3.11+, Rust, and maturin are
required:
The root smol_bytes module exposes Buffer, BytesMut, Utf8Buffer,
Utf8Bytes, and Utf8BytesMut; smol_bytes.shared and smol_bytes.compact
expose the immutable Bytes and Utf8Bytes strategy types.
The UTF-8 classes are string-like from Python: len(), indexing, and the
truncate/split_to/split_off/slice methods all work in Unicode
characters, while byte_len() and the explicitly byte-oriented Buf-style
methods (advance, get_*) work in bytes.
=
assert
assert == b
=
assert == 4 # Unicode characters
assert == 5 # UTF-8 bytes
assert ==
assert ==
Binding behavior worth knowing:
- Methods that allocate proportionally to caller data raise
MemoryErroron absurd or failing requests instead of aborting the interpreter, like CPython containers. memoryview(...)over the shared and compactBytesclasses exports a snapshot copy, not a live view.- Slice assignment on the mutable classes requires matching lengths, and contiguous assignments take a direct copy fast path.
JavaScript / WebAssembly
Install the published package with npm install smol-bytes.
To build from a checkout instead, install Node.js 20, wasm-pack 0.13.1, and
the wasm32-unknown-unknown target. The generated package is pinned to
wasm-bindgen 0.2.126 for reproducibility:
The Wasm build entry point is:
The root export contains the core, mutable, and shared UTF-8 types; the
smol-bytes/shared and smol-bytes/compact exports provide the strategy
types. Byte conversions at the Wasm boundary return copies, offsets are byte
offsets, and fallible operations throw catchable errors rather than trapping
the instance.
import { Utf8Bytes } from "smol-bytes";
import { Bytes as CompactBytes } from "smol-bytes/compact";
const raw = CompactBytes.fromBytes(new Uint8Array([1, 2, 3]));
console.assert(raw.isInline());
console.assert(raw.toBytes()[2] === 3);
const text = Utf8Bytes.fromString("café");
console.assert(text.len() === 5); // byte length
console.assert(text.toString() === "café");
Performance characteristics
Structural properties of the representations:
- Values of at most 62 bytes construct inline with no backing allocation.
- Every handle is 64 bytes;
Option<Bytes>is the same size asBytes. - Cloning an inline value copies 64 bytes; cloning a heap-backed immutable
value bumps a reference count; cloning
BytesMut/Utf8BytesMutcopies contents. - Shared heap-backed conversions to and from
bytes::Bytesreuse the backing allocation. - Compact storage copies at most 62 bytes when inlining a shrinking view.
Run the benchmark suite when investigating a change:
Development
These commands match the main CI checks:
See CONTRIBUTING.md for contribution guidance.
License
smol-bytes is available under either the MIT license or the Apache License,
Version 2.0, at your option. See LICENSE-MIT and
LICENSE-APACHE.
Copyright (c) 2026 Al Liu.