# puffin is an instrumentation profiler with a cheap off switch

> puffin is an Embark Studios profiling library for Rust where you mark code with macros, the marked scopes cost on the order of 50 to 200 nanoseconds, and turning the profiler off reduces that to about 1 nanosecond. The data is collected per thread, flushed by the application, and can be viewed in a separate app, over TCP, or inside the running game window.

**EmbarkStudios/puffin** — 🐦 Friendly little instrumentation profiler for Rust 🦀

- Repository: https://github.com/EmbarkStudios/puffin
- Stars: 1,754 · Forks: 112
- Language: Rust
- License: Apache-2.0
- Published: 2026-10-16 · Updated: 2026-10-16 · Language: en
- Canonical page: https://hysenlabs.com/projects/embarkstudios-puffin

## You mark the code, the profiler measures the marks

puffin is described as a friendly little instrumentation profiler for Rust, and it comes from Embark Studios. The mechanism is instrumentation rather than sampling, which means the profiler only knows about the regions of code you explicitly marked.

The API is two macros. `puffin::profile_function!()` marks a whole function, and `puffin::profile_scope!` marks a named region inside it, with a name and optionally a value.

```rust
fn my_function() {
    puffin::profile_function!();
    ...
    if ... {
        puffin::profile_scope!("load_image", image_name);
        ...
    }
}
```

The scope name and the optional payload are what turn a measurement into something you can read later, since the viewer can show a flamegraph of the marked regions and group them by name.

The cost of doing this is stated in the documentation, and it is the number that decides whether the approach is usable. The scopes are described as pretty light-weight, costing around 50 to 200 nanoseconds. For comparison, that is small against a frame budget and enormous against a hot inner loop, so where you put the scopes matters. Marking a function that runs once per frame is free in practice. Marking something called in a tight loop is a different decision.

The audience is a developer working on a Rust application, typically a game, who wants to know where the time goes inside a frame and who is willing to say so in the source.

## The off switch is what makes the instrumentation affordable

The second number in the documentation is the one that makes instrumenting a shipping application possible. You have to turn the profiler on before it captures any data, with a call to `puffin::set_scopes_on(true)`, and when the profiler is off the scope macros cost about 1 nanosecond on an M1 MacBook Pro, plus some stack space.

One nanosecond is the cost of a predictable branch. It is small enough that leaving the macros in the code and enabling the profiler only when you need it does not distort the thing you are measuring. That is the central design decision, and everything else in the library follows from it.

The consequence for a user is that the profiler is not something you turn on by starting a different build or attaching to a process. The instrumentation stays compiled in, and the decision is made at runtime, by a single function call somewhere in your startup path. A developer can wire it to a hotkey or a command line flag and capture a profile of the exact build the users are running.

There is a second piece of required wiring that is easy to miss. Once per frame you need to call `puffin::GlobalProfiler::lock().new_frame()`. That call is what tells the collector a frame boundary, which is what gives the viewer something to align a flamegraph against. The remote profiling example in the documentation repeats the same instruction in a comment, so it is clearly something the library expects the application to do regularly rather than once.

## Thread-local streams feed a global collector

The data flow explains the design of the library and is worth understanding before you wire it up.

The macros write data to a thread-local data stream. When the outermost scope of a thread is closed, that thread's data stream is sent to a global profiler collector. In other words, each thread accumulates its own measurements and hands the batch over when its outermost scope unwinds, and the global collector is what the viewer eventually reads.

That has two implications. The first is that a scope must be closed for the data to move, which is why the scope macros are designed around a lexical block and why the outermost scope closing is the flush point. The second is that the thread-local approach keeps the hot path free of contention, since threads are not locking a shared buffer while they are being measured.

The `new_frame()` call mentioned earlier is the second half of the pipeline. The global collector accumulates events, and the frame boundary is what separates one frame's worth of events from the next, so the viewer can show a timeline rather than one undifferentiated stream.

This is also why the library needs a place to put the data once the frames have been accumulated. That is what the other three crates in the workspace are for.

## puffin_http and puffin_viewer move profiles over TCP

The workspace has four members, and they cover the whole path from measurement to display.

`puffin` is the library itself, the macros and the collector. `puffin_egui` renders the profile data inside a running application, using the egui immediate mode interface, and the documentation shows a flamegraph produced that way. If you are using `eframe`, the egui repository has a puffin profiler example that shows the integration.

`puffin_http` and `puffin_viewer` are the remote pair. The HTTP crate sends profile events over TCP to the viewer, and setting it up is a handful of lines.

```rust
fn main() {
    let server_addr = format!("127.0.0.1:{}", puffin_http::DEFAULT_PORT);
    let _puffin_server = puffin_http::Server::new(&server_addr).unwrap();
    eprintln!("Run this to view profiling data:  puffin_viewer {server_addr}");
    puffin::set_scopes_on(true);

    // You also need to periodically call
    // `puffin::GlobalProfiler::lock().new_frame();`
    // to flush the profiling events.
}
```

The server binds a port, the application prints the address, and the separate viewer binary connects to it. The address is a plain `127.0.0.1` with the crate's default port in the example, which is a localhost setup, but nothing in the snippet suggests the transport is limited to it, and the address is passed in as a string, so a remote host is a matter of what you bind and what you connect to.

The important operational detail is in the closing comment. The events are not pushed continuously by the server, they are flushed by the application's `new_frame()` call, so an application that stops calling it stops sending.

## The workspace keeps binary compatibility on purpose

The Cargo manifest of the workspace explains a few decisions that would otherwise look arbitrary.

The workspace has four members, `puffin`, `puffin_egui`, `puffin_http` and `puffin_viewer`, on edition 2024 with a stated minimum Rust version of 1.95.0, and the licence is `MIT OR Apache-2.0`, so you take your pick of the two. The README says the same thing, dual licensed at your option, with both `LICENSE-APACHE` and `LICENSE-MIT` in the repository, and it adds that contributions are dual licensed the same way unless stated otherwise.

The dependency that explains itself is `bincode`, pinned to 1.3 with a comment that bincode 2 and later has a different default encoding, and that upgrading would break compatibility with existing `.puffin` files and with the `puffin_http` wire format. That is a deliberate constraint for anyone who keeps captured profiles on disk or reads them over the network, and it is a good example of the kind of thing a profiler has to think about that a game does not.

Two other entries are worth noting. `lz4_flex` and `zstd` are both present, so profile data can be compressed, and the workspace lints deny `unsafe_code` outright, with a set of warnings around lifetimes, future incompatibility and style.

The release process is also described, and it is worth a look because it is more disciplined than many. The project uses the `cargo release` tool to manage changelogs, git tags and publishing, each substantial pull request is expected to add an entry under the `[Unreleased]` section following keep a changelog, and the crate version in `Cargo.toml` is never updated manually in a pull request because `cargo release` handles it.

## Where puffin is the wrong tool

The limitation of an instrumentation profiler is the same one it has always had, and the project is honest enough to let the numbers speak.

The cost is 50 to 200 nanoseconds per scope, which is nothing at frame granularity and something significant in a tight loop. If the region you need to understand is not the one you marked, the profiler will not tell you about it, because it has no idea the region exists. That is a different failure mode from a sampling profiler, which sees the whole program whether or not you annotated it. A developer chasing a cost in code nobody instrumented will find puffin silent.

There is a second consequence. The instrumentation has to be in the source, which means the profiling is available in the build your users run, and enabling it is a runtime switch rather than an attach. That is a feature for a game, where the bug only reproduces on the user's machine, and a liability for a service where you would rather attach a profiler to a running container without shipping a modified binary.

The alternative, and the project points at it, is the `profiling` crate, which provides a unifying layer of abstraction on top of puffin and other profiling crates. The difference in approach is that instead of committing to one profiler and its macros, you call a shared API and the choice of backend becomes a configuration decision. If your team wants to switch profilers, or wants the same annotations to work under a different tool, that crate is the way to get there, at the cost of giving up direct access to the parts of puffin that are specific to it.

For a Rust game, a frame oriented profiler with an in-game view and a remote viewer is close to ideal, and the 1 nanosecond off state is what makes it practical to ship. For a long running service, the same design is the wrong shape.

## Conclusion

puffin suits a Rust project, most often a game, where the interesting question is which part of a frame takes the time and you are willing to annotate the code with scopes. It is the wrong tool for a service that needs continuous profiling of production traffic without code changes, and the wrong tool if you cannot add instrumentation to the hot path. Check first that your Rust toolchain meets the stated minimum of 1.95.0, because the workspace is on edition 2024, and read the `profiling` crate if you want one abstraction over several profilers rather than a commitment to this one.

## FAQ

### How much does puffin slow down my Rust code?

The documentation states that the scopes cost around 50 to 200 nanoseconds. When the profiler is off, which requires not calling `puffin::set_scopes_on(true)`, the scope macros have an overhead of about 1 nanosecond on an M1 MacBook Pro, plus some stack space.

### What do I need to do to use puffin?

Add `puffin::profile_function!()` or `puffin::profile_scope!("name")` to the code you want to measure, call `puffin::set_scopes_on(true)` to enable capture, and call `puffin::GlobalProfiler::lock().new_frame()` once per frame so the profiling events are flushed.

### How do I view a puffin profile?

Three ways. You can render the data inside the running application with the puffin_egui crate, you can send events over TCP with puffin_http and open them in the separate puffin_viewer binary, and the egui repository has a puffin profiler example for eframe based applications.

### What licence is puffin under?

Dual licensed, MIT OR Apache-2.0, at your option, with both LICENSE-MIT and LICENSE-APACHE in the repository. The README adds that any contribution is dual licensed the same way unless the contributor states otherwise.

## Sources

- [EmbarkStudios/puffin on GitHub](https://github.com/EmbarkStudios/puffin)
- [Issues](https://github.com/EmbarkStudios/puffin/issues)
- [License: Apache-2.0](https://github.com/EmbarkStudios/puffin/blob/main/LICENSE)
- [README](https://github.com/EmbarkStudios/puffin/blob/main/README.md)
- [Releases](https://github.com/EmbarkStudios/puffin/releases)

---

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