An asynchronous Redis client for Rust.
Documentation
Philosophy
- Low allocations
- Full async library
- Lock free implementation
- Rust idiomatic API
- Multiplexing as a core feature
Features
- Full documentation with multiple examples
- Support all documented Redis Commands up to and including Redis 8.8
- Async support (tokio)
- Different client modes:
- Single client
- Multiplexed client
- Pooled client manager (based on bb8)
- Automatic command batching
- Advanced reconnection & retry strategy
- Pipelining support
- Configuration with Redis URL or dedicated builder
- TLS support
- Transaction support
- Pub/sub support
- Sentinel support
- LUA Scripts/Functions support
- Cluster support (minimus supported Redis version is 6)
- Client-side caching support
Protocol Compatibility
Rustis uses the RESP3 protocol exclusively.
The HELLO 3 command is automatically sent when establishing a connection.
Therefore, your Redis server must support RESP3 (Redis ≥6.0+ with RESP3 enabled).
If you use Redis 5 or older, or your Redis 6+ server still defaults to RESP2,
Rustis will not work.
To verify your server supports RESP3:
redis-cli --raw HELLO 3
If you see server info (role, version, etc.), you're good to go. If you get an error, upgrade Redis.
Observability
Rustis emits tracing events and spans. Install any
subscriber to see them:
tracing_subscriber::fmt().with_max_level(tracing::Level::DEBUG).init();
Every event from the network task is wrapped in a connection span carrying a
tag field — host:port, or name:host:port when connection_name is set — so
output from several clients stays attributable without any per-message prefix.
Reconnections open a nested reconnect span, which groups the in-flight purge,
the retries and the subscription replay into one identifiable unit. In cluster
mode, events about a specific node carry a node field.
If you use log rather than tracing, you need to change nothing. Rustis
enables tracing's log feature, so every event also emits a log record and
existing env_logger-style setups keep working unchanged.
Levels follow the usual convention: error and warn for conditions that need
attention, info for connection lifecycle, debug for per-command traffic, and
trace for the message-queue internals.
Minimum Supported Rust Version
Rust 1.88, declared as rust-version in Cargo.toml and verified by a CI
job that compiles both runtimes with exactly that toolchain. Let chains hold the
floor there; edition 2024 on its own would allow 1.85.
Raising it is treated as a breaking change and is announced in CHANGELOG.md.
Safety
Rustis is #![forbid(unsafe_code)], and that is a deliberate position rather
than an accident of never having needed unsafe.
It costs less here than it would elsewhere. RESP is length-delimited, so the parser reads a header and skips the announced number of bytes instead of searching for delimiters — which leaves little for the usual payoff of unsafe in a parser, SIMD structural scanning, to find. Hardware CRC16 for cluster slots is the other candidate, and standalone clients skip slot computation entirely.
What it buys is that a malformed or hostile reply can never become a memory-safety bug. The real hostile-input surface is then panics and unbounded allocation, and both are addressed directly:
- The explicit-panic lint family (
unwrap_used,expect_used,panic,unreachable,todo,unimplemented) isdenycrate-wide, andclippy::indexing_slicingisdenyinresp/andnetwork/— the two zones where a panic is fatal rather than merely wrong. Surviving sites carry an#[allow(…, reason = "…")]naming the invariant that makes them unreachable. - Frame size, nesting depth and element counts are bounded and configurable
(
Config::limits), so a crafted reply cannot drive an unbounded allocation or a stack overflow. - Four
cargo-fuzztargets exercise the frame parser, both deserializers, and the chunked decode path.
Basic Usage
use rustis::{
client::Client,
commands::{FlushingMode, ServerCommands, StringCommands},
Result,
};
#[tokio::main]
async fn main() -> Result<()> {
// Connect the client to a Redis server from its IP and port
let client = Client::connect("127.0.0.1:6379").await?;
// Flush all existing data in Redis
client.flushdb(FlushingMode::Sync).await?;
// sends the command SET to Redis. This command is defined in the StringCommands trait
client.set("key", "value").await?;
// sends the command GET to Redis. This command is defined in the StringCommands trait
let value: String = client.get("key").await?;
println!("value: {value:?}");
Ok(())
}
Tests
- From the
redisdirectory, rundocker_up.shordocker_up.cmd - run
./run_tests.sh - run
cargo fmt --all -- --check
The test suite requires --test-threads=1: tests share a single Redis instance
and flush the database, so running them in parallel produces spurious failures.
That is the only thing run_tests.sh does beyond selecting the features —
cargo test --features tokio-rustls,pool,json,client-cache -- --test-threads=1.
Extra arguments are forwarded, so ./run_tests.sh string filters by name.
Benchmarks
- From the
redisdirectory, rundocker_up.shordocker_up.cmd - run
cargo bench