Lapin: an async AMQP 0-9-1 client for Rust that lets you pick the runtime
AMQP client library in Rust, with a clean, futures based API
At a glance
- What is it?
- lapin is a Rust client library for AMQP 0-9-1 aimed at RabbitMQ, built on an async I/O layer and made runtime-agnostic through Cargo feature flags. It is a good fit for teams already on tokio or smol, and a poor fit if you need AMQP 1.0 or a client that hides the runtime choice from you.
- Who is it for?
- Adopt lapin if your service already runs on tokio or smol and talks to RabbitMQ, because the runtime choice is a Cargo feature rather than a rewrite, and automatic recovery with wait_for_recovery removes the reconnect boilerplate. Do not adopt it if you need AMQP 1.0, a streaming protocol, or a client that hides the executor from you, since lapin implements the AMQP 0-9-1 specification and nothing beyond it.
- Can I use it commercially?
- Yes. MIT 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 4 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 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
An AMQP 0-9-1 client that assumes RabbitMQ
lapin implements the AMQP 0-9-1 specification on top of an async I/O layer, and the project describes itself as targeting RabbitMQ. That combination is the first thing to understand about it, because it sets both the scope and the audience. AMQP 0-9-1 is the protocol RabbitMQ speaks, with its exchange, queue, binding and acknowledgment model, and writing against that model from Rust means the API looks like queues and consumers rather than streams and subscriptions.
The library is async first and futures based. There is no blocking mode, and no thread per connection model to fall back on. Consumers are Streams, so the code that handles an incoming delivery is written as an async loop.
The other headline property is runtime-agnosticism. The same code works with tokio, which is the default, with smol, or with async-global-executor. That is a real constraint on the design rather than a marketing claim, and it is visible in the Cargo.toml, where each runtime is a feature that forwards to the amq-protocol and async-rs crates.
The crate is at version 4.12.2, on edition 2024, with a declared minimum rust-version of 1.88.0. Licence is MIT. The last push to the repository was on 2026-10-18, and the repository has no GitHub releases, so version tracking happens on crates.io rather than through the release page.
The quick start path: declare, publish, consume, ack
The README example is a complete program, and it is worth reading closely because the shape of the API is visible in it. The connection address comes from an environment variable with a local default:
use lapin::{
options::*, types::FieldTable, BasicProperties, Connection,
ConnectionProperties, Result,
};
#[tokio::main]
async fn main() -> Result<()> {
let addr = std::env::var("AMQP_ADDR")
.unwrap_or_else(|_| "amqp://127.0.0.1:5672/%2f".into());
let conn = Connection::connect(&addr, ConnectionProperties::default()).await?;
let channel = conn.create_channel().await?;The channel is then used to declare a durable queue and publish to it. Note the two awaits on the publish call, which is the publisher confirm handshake rather than a duplicated call:
channel
.queue_declare("hello".into(), QueueDeclareOptions::durable(), FieldTable::default())
.await?;
channel
.basic_publish(
"".into(),
"hello".into(),
BasicPublishOptions::default(),
b"Hello, world!",
BasicProperties::default(),
)
.await?
.await?;Consumption follows the same option-builder style, and each delivery is acknowledged explicitly:
let mut consumer = channel
.basic_consume(
"hello".into(),
"my_consumer".into(),
BasicConsumeOptions::default(),
FieldTable::default(),
)
.await?;
while let Some(delivery) = consumer.next().await {
let delivery = delivery?;
delivery.ack(BasicAckOptions::default()).await?;
}The pattern to take away is that every protocol operation takes an options struct and a FieldTable, and every result is a Result, so errors surface through the same ? operator as the rest of your Rust code.
Automatic recovery, and the wait that makes it usable
Network failures are the normal case for a long-lived consumer, and lapin has a recovery mode. The documentation states that passing .enable_auto_recover() in ConnectionProperties makes lapin automatically reconnect and replay exchanges, queues, bindings, and consumers after a network failure.
use lapin::ConnectionProperties;
let props = ConnectionProperties::default().enable_auto_recover();Replaying the topology matters as much as the reconnect itself. A consumer that reconnects to a broker that no longer knows about its queue is a consumer that silently receives nothing, so the queue and binding declarations are redone as part of recovery.
The second half is how the application learns that recovery has finished. The documentation says that after catching a recoverable error on a channel, you call channel.wait_for_recovery(error).await, and that this blocks until recovery is complete. Without that call the application continues against a channel whose state is unknown, which is the failure mode recovery is supposed to remove.
The repository also ships two example programs that show the alternative postures, examples/reconnect_manual.rs and examples/reconnect_semi_automatic.rs, next to examples for consumers, publishers, pubsub, client certificates and per-runtime variants. The presence of a manual variant tells you the automatic mode is a choice rather than the only behaviour.
Runtime choice is a Cargo feature, and a custom runtime is a function call
The runtime is selected at compile time. The Cargo features table lists tokio as the default, and states that it requires a running Tokio runtime, with smol and async-global-executor as the alternatives. In Cargo.toml the mapping is explicit, with each flag forwarding to the protocol crate and to async-rs:
default = ["rustls", "default-runtime"]
default-runtime = ["tokio"]The default feature set pulls in rustls and the tokio runtime, so a project that depends on lapin with no feature flags gets TLS and Tokio already wired.
The escape hatch is connect_with_runtime, which takes an async_rs::Runtime value and lets you supply a runtime the crate did not anticipate:
use lapin::{Connection, ConnectionProperties, Result};
async fn connect_with_custom_runtime() -> Result<()> {
let runtime = async_rs::Runtime::tokio_current();
let conn = Connection::connect_with_runtime(
"amqp://localhost",
ConnectionProperties::default(),
runtime,
).await?;
drop(conn);
Ok(())
}That function is what makes the runtime-agnostic claim concrete. The library is not generic over an executor trait that you must satisfy; it accepts a runtime value from the async-rs crate, and the documentation points at async-rs for the list of available wrappers.
TLS is a two step decision and the provider choice can break your build
The TLS configuration has three separate decisions, and the documentation is explicit that picking the wrong combination causes runtime errors rather than a compile failure.
First, the TLS backend. rustls is the default, native-tls uses the platform native library, and openssl uses OpenSSL. The rule is to pick at most one.
Second, the certificate store, which applies only when rustls is active. rustls-platform-verifier is the default and uses the platform trust store, rustls-native-certs uses native root certificates, and rustls-webpki-roots-certs uses the bundled webpki root set.
Third, the rustls crypto provider, and here at least one is required. The default is aws-lc-rs, and the alternative is ring, which the table describes as more portable. The Cargo.toml carries the same warning in a comment: choose at least one, otherwise runtime errors, and it adds that aws-lc-rs is the default but does not build everywhere.
That comment is the practical takeaway for anyone deploying on an unusual target. A default build on a mainstream platform is fine, and the moment you cross compile to a platform where aws-lc-rs is unavailable, the fix is the rustls--ring feature rather than a change of TLS backend.
For platforms that need OpenSSL with a vendored copy, the Cargo.toml also lists a vendored-openssl feature alongside the plain openssl one.
Three small features that change debugging behaviour
The miscellaneous feature flags are worth knowing because each of them changes what you see when something goes wrong.
codegen forces protocol code regeneration at build time. The repository contains a build.rs, a regen-code.sh script, a templates directory and a code generation crate behind the codegen-internal feature, so the protocol code is generated rather than handwritten. Pulling that in during a normal build is a debugging aid rather than something to leave enabled in production.
hickory-dns switches in the Hickory DNS resolver, and the table describes it as avoiding spurious network hangs. That is a specific failure mode: a resolver that blocks for a long time turns a connect into a stall, and the default resolver on some platforms behaves that way.
verbose-errors turns on more detailed AMQP parser error messages, which matters most when a broker rejects a frame and the protocol error would otherwise be a bare status code.
The crate metadata also pins the toolchain floor, which matters when you upgrade:
name = "lapin"
version = "4.12.2"
edition = "2024"
rust-version = "1.88.0"Edition 2024 and a 1.88.0 minimum mean an older pinned toolchain in your build image will fail to compile the crate, and that is a build environment change rather than an application change. The CHANGELOG.md at the repository root is where the migration notes for a version jump would live.
Where lapin stops, and how it differs from a blocking client
The boundary is the protocol. lapin implements AMQP 0-9-1 and nothing beyond it, so a broker that only speaks AMQP 1.0, or a use case built on a streaming protocol, is out of scope. The same applies to the feature-flag world: there is one runtime family to choose from and one TLS family, and the choice has to be made at compile time rather than swapped at runtime.
The comparison worth making is with a blocking AMQP client, not with another Rust library, since the difference is architectural. A blocking client owns a thread per connection or per consumer and hands you callbacks. lapin owns no threads of its own, hands you a Stream of deliveries, and leaves the executor to the runtime feature you selected. In exchange for that, your application now has a lifecycle responsibility: nothing runs when the runtime is not running, and a consumer loop that awaits the wrong thing stalls silently rather than blocking a worker.
Another practical difference is error handling. Recoverable channel errors are values you can catch and feed to wait_for_recovery, which is a Rust shaped way of handling a condition that in a callback client is usually a status code you have to inspect yourself.
For an application that publishes and consumes against RabbitMQ with tokio, that model is a good fit. For a library author who wants one client that works everywhere, or for a broker that is not RabbitMQ, it is not.
Editorial conclusion
Adopt lapin if your service already runs on tokio or smol and talks to RabbitMQ, because the runtime choice is a Cargo feature rather than a rewrite, and automatic recovery with wait_for_recovery removes the reconnect boilerplate. Do not adopt it if you need AMQP 1.0, a streaming protocol, or a client that hides the executor from you, since lapin implements the AMQP 0-9-1 specification and nothing beyond it. Before you build, check the rustls crypto provider you enable, because the default aws-lc-rs provider is documented as not building everywhere, and the ring provider is the portable choice.
Frequently asked questions
Which async runtime does lapin use by default?
Tokio is the default. The feature table lists tokio as requiring a running Tokio runtime, with smol and async-global-executor as alternatives, and the default feature set in Cargo.toml is rustls plus default-runtime, where default-runtime is tokio.
How does lapin handle a network failure during a long-running consumer?
Passing .enable_auto_recover() in ConnectionProperties makes lapin automatically reconnect and replay exchanges, queues, bindings and consumers after a network failure. After catching a recoverable channel error, call channel.wait_for_recovery(error).await to block until recovery completes.
Why does my lapin build fail on a non-standard target?
The default rustls crypto provider is aws-lc-rs, and the Cargo.toml notes that it is the default but does not build everywhere, while ring is the more portable provider. The crate also requires at least one crypto provider, otherwise you get runtime errors.
Can lapin be used without Tokio?
Yes. You can select the smol or async-global-executor feature instead of tokio, or supply an async_rs::Runtime value to Connection::connect_with_runtime, and the documentation points at the async-rs crate for the list of available runtime wrappers.
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/amqp-rs-lapin)Was this useful?