# lilos: an async RTOS for Cortex-M that fits in two kilobytes of flash

> lilos is a small real-time operating system written in Rust to bring the async style of programming to microcontrollers, and its selling point is the size: about two kilobytes of flash and forty bytes of RAM before your tasks are added. The author has run a dozen real systems on it since 2019, and the repository is arranged so that the operating system, its optional extras and the test suite are separate crates with their own build settings.

**cbiffle/lilos** — A wee async RTOS for Cortex-M

- Repository: https://github.com/cbiffle/lilos
- Stars: 833 · Forks: 46
- Language: Rust
- License: MPL-2.0
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/cbiffle-lilos

## Two kilobytes of flash and forty bytes of RAM

lilos is described as a wee operating system written to support the async style of programming in Rust on microcontrollers, and the numbers are given up front. It fits in about two kilobytes of flash and uses about forty bytes of RAM before your tasks are added.

What you get in that space is a full async runtime with multiple tasks, support for complex concurrency through join and select, and a set of convenient but simple APIs. The point of the size figure is not a benchmark, it is a budget: a microcontroller with tens of kilobytes of flash can afford the runtime, and one with a couple of kilobytes cannot, and the project is designed so that the first number stays true.

The other claim in the README is a field one. The system has been deployed in real embedded systems since 2019, running continuously, and the author says he has built about a dozen systems around it of varying complexity on half a dozen varieties of microcontroller, and then concludes that it works pretty okay. That is a refreshingly low-key way to describe seven years of production use, and it is more informative than a benchmark table would be, because it says the thing survives contact with real hardware and real deadlines.

There is an intro guide in the documentation directory and a set of example programs, and the README points at both as the place to see what a program in this system looks like.

## The operating system, the extras and the test suite are separate crates

The repository layout is described in a way that tells you something about the design, because the map is not the usual one.

The os directory holds the operating system crate itself. The testsuite directory holds a test suite for the operating system that can run on a Cortex-M0 or better, with its own readme of instructions. The examples directory holds example programs for various microcontrollers. And the extra directory holds what the README calls non-core crates, providing features that do not need to be in the operating system proper.

Those extras are worth listing, because they are the pieces a real program needs and a minimal runtime does not have. The handoff crate provides a synchronous rendezvous for transferring data from one task to another with minimal copies. The semaphore crate is a counting semaphore. The rwlock crate is a read-write lock. And the workspace dependency list shows two more, a list crate and a watch crate, with a separate entry point for the watch crate in the dependencies.

That is the shape of the design philosophy in one directory listing. The core stays small enough to keep the size promise, and the conveniences live beside it, so a program that needs a read-write lock pays for the read-write lock and a program that does not pays nothing.

The example programs are chosen to show the range: there are directories for the RP2040, for an STM32F4, for an STM32G031 and for an STM32H7, which covers a low cost microcontroller, a mainstream Cortex-M4 part, a Cortex-M0+ part and a high end Cortex-M7 part.

## Why one cargo command will not build the workspace

The building section contains the sentence that will save a newcomer an hour: the subdirectories use their own Cargo configuration files to change settings that Cargo has so far declined to allow in the project manifest, such as the target triple. That means you need to change into the subdirectories to build things, rather than using a single workspace build command.

That is not an inconvenience, it is a consequence of what the project is. A cross-compiled microcontroller runtime has to be told which target it is being built for, and Cargo does not have a way to express a per-member target triple in the workspace manifest. The project worked around the limitation by putting the configuration in the places Cargo does allow it, which means the build is driven by where you are rather than by a flag.

The consequence for a user is simple. If you are using lilos in your own application, none of this applies: the usual way is to add the crate to your project with the cargo add command, and the workspace machinery stays out of the way entirely. If you are working on lilos itself, you move into the directory you care about and build there.

The README is explicit that the build instructions are mostly for building the examples or for working on the operating system itself, which is a useful signal about who the instructions are for.

## Using it, and working on it, are two different workflows

The two workflows are laid out separately in the README, and separating them is the clearest way to understand the project.

To use lilos in a program, you add it as a dependency. That is the whole procedure:

```bash
cargo add lilos
```

To work on lilos, you need a Rust toolchain installed through rustup, because the project pins the toolchain with a rustup style toolchain file to make sure you get the right results. The README notes that rustup will automatically ensure the right toolchain is available, including support for the right target architecture for whatever you are building.

The sequence is then to enter the directory you are interested in and build:

```bash
cd os
cargo build
```

The README adds that a release build gives smaller binaries, which matters for a system whose selling point is its size. To try an example on a microcontroller evaluation board, you look at the readme in that example's directory, and in most cases running the program in that directory is enough.

And to build everything in the repository, there is a single script:

```bash
./build-all.sh
```

So the repository ships three other scripts of the same shape for the other chores, one for running the linter across the workspace, one for building the documentation and one for checking the minimum supported Rust version across it. That is the standard shape of a Rust project that takes its toolchain policy seriously, and it is visible from the root directory listing alone.

## The dependency list is a map of the hardware constraints

The workspace dependency list is short, and each entry explains a decision.

The core is a pair of support crates for the Cortex-M ecosystem, one with the inline assembly feature enabled, and a runtime crate and a semihosting crate alongside them. There is a configuration-if crate for conditional compilation, a pinning crate for the executor internals, two panic crates, one that halts and one that uses semihosting, and a scope guard crate.

The futures crate is the interesting one. It is pinned with default features turned off and only the async-await feature enabled, which is the difference between a futures runtime that assumes a full standard library and one that fits on a microcontroller.

Then there are the vendor support packages, the peripheral access controller packages for the STM32 and the RP2040 families, and the RP2040 boot crate, which is the second stage bootloader for that part. the peripheral access crates are generated, so the version numbers track the silicon.

The result is a dependency tree with no operating system underneath it, no allocator in sight and no futures features that presume a hosted environment. Reading the list is a reasonable way to understand what the runtime is prepared to do and, just as usefully, what it has decided not to carry.

## The size budget is enforced by the build profile, and the minimum Rust version is a real constraint

Two details in the project manifest show how seriously the size budget is taken.

The first is the development profile. It is configured for space reasons, and the comments explain each setting. Code generation units are set to one, which gives better optimisation. Debug information stays on, because symbols are nice and do not increase the size on flash, which is the part that matters for a microcontroller. Link time optimisation is enabled, and the optimisation level is set to one, described as very basic optimisation, because even in a development build you want the binary to fit.

The second is a comment attached to the workspace lints table, which says that before you get excited about turning it on, note that it is silently ignored by the minimum supported Rust version of 1.69. That is a trap avoided in public, and it is worth internalising that the workspace declares a minimum supported Rust version of 1.69 and an edition of 2021, while the toolchain file pins something newer for the actual development.

The test suite has a similar constraint written into the manifest. The comment says the test suite is not tiny and does not fit in a 32 kilobyte Cortex-M0 target without cranking up the optimisation level, which is why there is a separate package profile for it.

Taken together with the release notes file and the helper scripts for linting, documentation and the minimum version check, this is a repository that has been run in anger since 2019, and the licence is MPL-2, which is the Mozilla Public License, per file, the same licence the author is used to from other work.

## Conclusion

Adopt lilos if you are writing a microcontroller program in Rust and want the async runtime you already use on a desktop to work on a Cortex-M target without dragging in a general purpose RTOS. Do not adopt it if you need the breadth of a conventional scheduler, since the concurrency primitives are limited to a join, a select and a small set of extra crates. Before you build, remember that the repository cannot be built with a single workspace command and that each subdirectory carries its own Cargo configuration, and check the minimum supported Rust version, which the workspace declares as 1.69, since some workspace settings are silently ignored below it.

## FAQ

### What is lilos and how big is it?

It is a small operating system written in Rust to support async programming on microcontrollers. It fits in about two kilobytes of flash and uses about forty bytes of RAM before your tasks are added, and in that space you get an async runtime with multiple tasks, join and select, and a set of simple APIs.

### How do I add lilos to my Rust project?

With cargo add lilos, which is the usual way to use it. The build instructions in the repository are mostly for building the examples or for working on the operating system itself, and those need a rustup toolchain because the project pins the toolchain version with a toolchain file.

### Why can I not build the whole lilos workspace with one command?

Because the subdirectories carry their own Cargo configuration files, which the README explains is how the project changes settings Cargo does not allow in a workspace manifest, such as the target triple. You therefore change into the directory you want to build, and the repository provides a build-all script for building everything at once.

### Which extra crates ship alongside the lilos core?

The extra directory holds crates that do not need to be part of the operating system: a handoff crate providing a synchronous rendezvous between tasks with minimal copies, a counting semaphore, a read-write lock, and the workspace also lists list and watch crates.

### What is the minimum supported Rust version for lilos?

The workspace declares a minimum supported Rust version of 1.69 and edition 2021. The manifest also notes that the workspace lints table is silently ignored at that version, so lint rules that look enabled in the manifest may not be in effect on an older toolchain.

## Sources

- [cbiffle/lilos on GitHub](https://github.com/cbiffle/lilos)
- [Issues](https://github.com/cbiffle/lilos/issues)
- [License: MPL-2.0](https://github.com/cbiffle/lilos/blob/main/LICENSE)
- [README](https://github.com/cbiffle/lilos/blob/main/README.md)

---

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