heapless: fixed-capacity Rust containers for targets with no allocator
Heapless, `static` friendly data structures
At a glance
- What is it?
- heapless provides data structures whose capacity is part of the type, so a bare-metal Rust build can hold buffers in a `static` without linking an allocator. Every trait integration is a Cargo feature, and one of the atomic backends carries a single-core soundness warning worth reading before you enable it.
- Who is it for?
- Heapless fits firmware that cannot link a global allocator and wants its buffers declared as `static` values with a footprint fixed at build time. It does not fit code that already allocates and takes unbounded input, where the standard Vec is simpler.
- Can I use it commercially?
- Yes. Apache-2.0 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 8 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 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Why a fixed capacity container matters on a target with no allocator
Embedded targets frequently ship without a global allocator, so any collection that grows on demand is out of reach before your code compiles. heapless puts the capacity into the type instead, producing a fixed-size value that can live in a `static` and whose bound is visible in the signature. Nothing has to be reserved, moved or reclaimed while the program runs.
The intended reader is narrow: Rust code built for `no-std` targets. The package categories are data-structures and no-std, the keywords are static and no-heap, and the repository topics are data-structures, no-std and rust. The description states the goal in a single line, data structures that don't require dynamic memory allocation. Development and maintenance sit with the libs team, the rust-embedded working group that stewards other bare-metal crates, and Cargo.toml names Jorge Aparicio, Per Lindgren and Emil Fresk as authors at version 0.9.3.
What you trade away is tolerance for input of unknown length. No allocator sits behind the container to fall back on, so a design whose input size nobody can predict has to be reshaped before this crate becomes useful. That constraint is the point of the project rather than a defect in it, and it is cheaper to check against your real input sizes early than to discover it during review.
Every optional trait rides behind a Cargo feature flag
Almost nothing is switched on by default. The manifest keeps the base crate free of dependencies and hangs each integration on a named feature, so what a firmware build pulls in is decided in one place. Serialization arrives as `serde`, which maps to dep:serde_core rather than the whole serde crate, and `ufmt`, which pulls in dep:ufmt and dep:ufmt-write together.
[features]
bytes = ["dep:bytes"]
serde = ["dep:serde_core"]
mpmc_large = []Logging on small targets goes through `defmt`, which implements the defmt::Format trait, and `zeroize` wires in the Zeroize trait so buffers can be wiped after use. Byte-slice interoperability sits behind `bytes`, and stream output behind the feature named `embedded-io-v0.7`, which depends on embedded-io. A feature that is absent from the build does not compile in: the trait impls are conditional, so you choose per target rather than per binary.
`mpmc_large` is an empty feature list whose comment reads Enable larger MPMC sizes, which confirms that queue width is a compile-time decision in this crate rather than something you tune after boot. A later entry carries the comment Implement some alloc Vec interoperability, and the feature name after it is cut off mid-word, so read that gap before assuming the crate has no bridge to allocator-backed vectors. The comment says there is one.
Portable atomics carry a single-core soundness warning
Three manifest features exist to give atomics to cores that lack them, and one of them carries an explicit warning inside the file. `portable-atomic` adds the portable-atomic dependency, whose own documentation asks you to enable something so it knows how to emulate. `portable-atomic-critical-section` adds the critical-section backend on top of it. `portable-atomic-unsafe-assume-single-core` selects the unsafe assume-single-core backend instead.
The warning attached to that third feature says it is only sound for single-core bare-metal privileged-mode targets. That is the failure mode to understand before turning it on. On a part with more than one core the operations it assumes are atomic are not, and no runtime check will report the mistake, because the compiler trusted the promise at build time and generated code around it.
Between the two backends, one masks interrupts around the atomic and the other simply trusts the target description. Neither is a matter of taste. The critical-section route adds interrupt latency around every contended access, while the unsafe route removes that cost at the price of correctness on hardware with more than one core. Pick the one your hardware description supports, and leave the third feature off on any part you cannot describe with confidence.
The README gives test commands but no install command
The README carries crates.io version and download badges and a link to docs.rs, yet it contains no command for adding the crate to a project and no dependency snippet to copy. To bring it into a build, the declaration at the top of Cargo.toml is the specification: name heapless, version 0.9.3, edition 2021, and a declared floor of rust-version 1.88.
For work inside the crate, two commands are documented. The first runs the whole suite with the serde feature enabled, and the second narrows the run to a single area by passing histbuf as a filter.
$ # run all
$ cargo test --features serde
$ # run only for example histbuf tests
$ cargo test histbuf --features serdeFormatting is the rougher edge for a newcomer. The project uses rustfmt with options that the README calls currently available only on the nightly channel, so formatting needs a nightly toolchain rather than whatever stable toolchain sits in your rustup default.
$ cargo +nightly fmt --allThe root file list explains part of why formatting is unusual here: both .rustfmt.toml and rustfmt.toml sit at the top level, two configuration files for one formatter. Check which one rustfmt actually loads before you commit a reformat, because in a crate this style-sensitive a large diff is harder to review than the change you meant to make.
The stated MSRV policy and the manifest floor disagree
The README carries an MSRV Policy section, and the rule in it moves with the toolchain: the crate is guaranteed to compile with the latest two stable versions of Rust. The worked example makes the window concrete, since if the latest stable is 1.70 then 1.69 and 1.70 are both covered. That is a promise about the present moment, not a fixed floor you can pin in a build file.
The manifest states something firmer. rust-version is 1.88, alongside edition 2021. Read together the two documents do not agree, and the distance between them is not marginal: the policy example talks about 1.69 and 1.70, while the manifest demands 1.88. Anyone quoting the policy text to justify an older compiler in CI will be wrong for this crate, so check rust-version against your pinned toolchain before anything else, because an outdated rustup default fails at the version gate long before a single heapless API is involved.
The licensing fields disagree as well: the repository header names Apache-2.0, the manifest declares MIT OR Apache-2.0, and both LICENSE-APACHE and LICENSE-MIT sit at the top level with the README offering either at your option. On cadence, the last push landed on 2026-10-06, and the tags run v0.9.3 on 2026-04-21, v0.9.2 on 2026-03-28 and v0.9.1 on 2025-08-21.
cfail and suppressions.txt guard the compile-time contract
Two root entries go unexplained anywhere in the README. The directory named `cfail/` is the conventional home for compile-fail tests, and `suppressions.txt` sits beside it. Neither name appears in the README, which covers the crate identity, the docs link, tests, formatting and licensing, and stops there.
That omission hides the part of the project most closely tied to its own claims. A crate whose argument is that misuse gets rejected by the compiler needs tests that break when rustc stops rejecting it, and a directory named for compile failures is where such tests sit. The root also carries `build.rs`, a `tests/` directory and a `src/` directory, so that work sits next to the ordinary unit and integration tests instead of in a separate repository.
The documented commands show how narrow you can make a run. A whole-crate pass uses cargo test --features serde, and one area is reached by passing a filter, with histbuf given as the example. When you add a type with an overflow contract or an empty-case contract, running just that filter is the cheap first check before the full suite.
Interoperating with allocator-backed Vec is a feature, not the default
The obvious alternative is not exotic. Link a global allocator and use the standard Vec, and nearly everything this crate offers becomes available with unbounded capacity. The difference in approach is where capacity lives: a Vec holds a pointer, asks the allocator for more room when it fills, and pays for that indirection on every growth. This crate's description puts capacity in the value itself, which is what allows an instance to sit in a `static` and keeps the footprint fixed before the program runs.
The manifest does not pretend the two worlds are separate. An entry near the end of the file carries the comment Implement some alloc Vec interoperability, so code already working with allocator-backed vectors has a declared bridge to the bounded types once that feature is enabled. No allocator is named among the optional dependencies, so the heap-free configuration is the default shape of the crate rather than an escape hatch.
The decision is therefore not ideological. Where an allocator is unavailable, or where a fixed footprint matters in a firmware image, these containers earn their keep. Where neither constraint holds and inputs are unbounded, the standard collection is the honest choice, and a bounded buffer would only be hiding an assumption about input length that the rest of the program still has to honour.
Editorial conclusion
Heapless fits firmware that cannot link a global allocator and wants its buffers declared as `static` values with a footprint fixed at build time. It does not fit code that already allocates and takes unbounded input, where the standard Vec is simpler. Verify three things first: rust-version 1.88 against your pinned toolchain, whether your part really is single-core before enabling portable-atomic-unsafe-assume-single-core, and how much of your own input path you would have to reshape as bounded.
Frequently asked questions
How do I add heapless to a no_std Rust project?
Cargo.toml declares the package as heapless version 0.9.3, edition 2021, with rust-version 1.88. The README contains no dependency line to copy, only crates.io badges and a link to docs.rs, so the manifest fields are the specification you add to your own Cargo.toml. Extra traits are opt-in through features such as `serde` and `defmt`.
Does heapless need a global allocator?
No. The package description calls them `static` friendly data structures that don't require dynamic memory allocation, and the categories are data-structures and no-std. A later manifest entry offers interoperability with alloc Vec types behind a feature, for code that does link an allocator.
What does the mpmc_large feature change in heapless?
The manifest lists mpmc_large with an empty dependency list and the comment Enable larger MPMC sizes. It is a marker feature, so enabling it changes what compiles into your build rather than pulling in another crate.
Is portable-atomic-unsafe-assume-single-core safe on a multicore board?
The manifest carries a warning that this option is only sound for single-core bare-metal privileged-mode targets. On a multicore part the atomics it assumes are not atomic, and the mistake is not caught at runtime. The alternative is portable-atomic-critical-section, which locks through critical sections instead.
How current is heapless and which Rust version does it require?
The last push was on 2026-10-06 and the most recent tag is v0.9.3 from 2026-04-21. Cargo.toml sets rust-version to 1.88, even though the README's MSRV policy describes a moving window covering the latest two stable versions of Rust.
Official sources
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.
[](https://hysenlabs.com/projects/rust-embedded-heapless)Was this useful?