Getting Started with Rust and Redis using redis-rs

Last updated: April 13, 2025

1. Introduction: Rust and In-Memory Data Stores

Redis is an extremely popular open-source, in-memory data structure store, often used as a high-performance cache, message broker, and database. Its speed stems from keeping data primarily in RAM. When building applications in Rust, you often need to interact with Redis for caching, session management, real-time leaderboards, or simple key-value storage.

The redis-rs crate is the most widely used and maintained Rust client library for Redis. It provides both synchronous and asynchronous APIs for interacting with a Redis server.

This guide covers the basics of setting up redis-rs, connecting to a Redis instance, and performing common operations like setting and getting values, using both the synchronous and asynchronous (Tokio-based) interfaces.

2. The redis-rs Crate

redis-rs provides a convenient and type-safe way to send Redis commands and handle responses. Key features include:

  • Support for a wide range of Redis commands.
  • Both synchronous and asynchronous (via feature flags) clients.
  • Connection pooling (optional feature).
  • Support for Redis Cluster, Sentinel, Pipelining, and Pub/Sub.
  • Conversion between Redis replies and Rust types.

3. Prerequisites

  • Rust and Cargo installed (see Getting Started with Rust).
  • A running Redis instance. The easiest way locally is via Docker:
    docker run -d --name my-redis -p 6379:6379 redis:alpine
    This starts Redis and exposes the default port 6379.
  • Basic understanding of Redis commands (like SET, GET).
  • (For async section) Familiarity with Rust's async/await and Tokio.

4. Project Setup

Create a new Rust binary project:

cargo new rust_redis_example
cd rust_redis_example

Add the redis crate to your Cargo.toml:

[package]
name = "rust_redis_example"
version = "0.1.0"
edition = "2021"

[dependencies]
redis = { version = "0.25", features = [] } # Check crates.io for latest version
# Add features later for async or pooling

Or use cargo add:

cargo add redis@0.25

We start with no extra features enabled for the synchronous examples.

5. Synchronous Usage

The default client provided by redis-rs is synchronous.

5.1 Connecting Synchronously

You create a Client and then get a Connection from it.

use redis::Commands; // Trait needed for commands like set/get

fn connect_sync() -> redis::RedisResult {
    let redis_url = "redis://127.0.0.1:6379/"; // Default Redis URL
    let client = redis::Client::open(redis_url)?; // Create client
    let connection = client.get_connection()?; // Get synchronous connection
    println!("Synchronously connected to Redis!");
    Ok(connection)
}

Note the use of redis::RedisResult and the ? operator for error handling.

5.2 Running Basic Commands (SET, GET)

Once you have a connection, you can execute Redis commands using methods provided by traits like redis::Commands.

// Inside a function that has `mut connection: redis::Connection`
// Use the `Commands` trait methods
// redis::cmd("SET").arg("my_key").arg("my_value").query(&mut connection)?; // Generic command

// Use convenience methods from the Commands trait
let _: () = connection.set("user:1:name", "Alice")?; // SET command
println!("Set user:1:name to Alice");

let name: String = connection.get("user:1:name")?; // GET command
println!("Retrieved user:1:name: {}", name);

// Get a non-existent key (returns RedisError NotFound)
match connection.get::<_, String>("non_existent_key") {
    Ok(value) => println!("Unexpected value: {}", value),
    Err(e) => {
        if e.kind() == redis::ErrorKind::TypeError { // Type error indicates key not found for String conversion
             println!("Key 'non_existent_key' not found (as expected).");
        } else {
            eprintln!("Error getting non_existent_key: {}", e);
        }
    }
}

Commands like set and get are generic and require type annotations (like let name: String = ... or connection.get::<_, String>(...)) so Rust knows what type to expect from Redis.

5.3 Complete Synchronous Example

Combine connection and commands in src/main.rs:

use redis::{Client, Commands, Connection, RedisResult};

// Connect function from above
fn connect_sync() -> RedisResult {
    let redis_url = "redis://127.0.0.1:6379/";
    let client = Client::open(redis_url)?;
    let connection = client.get_connection()?;
    println!("Synchronously connected to Redis!");
    Ok(connection)
}

// Function to run Redis commands
fn run_redis_commands(connection: &mut Connection) -> RedisResult<()> {
    // SET command
    let _: () = connection.set("sync_key", "Hello Sync Redis!")?;
    println!("SET sync_key");

    // GET command
    let value: String = connection.get("sync_key")?;
    println!("GET sync_key: {}", value);

    // INCR command
    let new_count: i32 = connection.incr("counter", 1)?; // Increment counter by 1
    println!("INCR counter: new value is {}", new_count);

    Ok(())
}

fn main() {
    println!("Running synchronous Redis example...");
    match connect_sync() {
        Ok(mut conn) => { // Connection needs to be mutable to run commands
            if let Err(e) = run_redis_commands(&mut conn) {
                eprintln!("Error running Redis commands: {}", e);
            }
        }
        Err(e) => {
            eprintln!("Error connecting to Redis: {}", e);
        }
    }
}

Run with cargo run (ensure Redis is running via Docker).

6. Asynchronous Usage with Tokio

For non-blocking applications, especially web servers, you'll want to use the asynchronous API.

6.1 Enabling Async Feature

First, enable the necessary features for redis-rs in Cargo.toml:

[dependencies]
# Enable tokio-comp feature for redis
redis = { version = "0.25", features = ["tokio-comp"] }
tokio = { version = "1", features = ["full"] }
# Add other needed dependencies like anyhow or serde if needed
anyhow = "1.0"

Or use cargo add:

cargo add redis@0.25 --features tokio-comp
cargo add tokio --features full
cargo add anyhow # For easier error handling

The tokio-comp feature enables compatibility with the Tokio runtime.

6.2 Connecting Asynchronously

Connecting asynchronously involves getting an AsyncConnection.

use redis::aio::Connection; // Use redis::aio for async
use redis::{Client, RedisResult};
use anyhow::Result; // Using anyhow for main function Result

async fn connect_async() -> RedisResult {
    let redis_url = "redis://127.0.0.1:6379/";
    let client = Client::open(redis_url)?;
    // Get asynchronous connection
    let connection = client.get_async_connection().await?;
    println!("Asynchronously connected to Redis!");
    Ok(connection)
}

6.3 Running Commands Asynchronously

Commands are run using the same Commands trait methods, but now they need to be .awaited.

// Inside an async function with `mut connection: redis::aio::Connection`
// use redis::AsyncCommands; // Trait needed for async set/get

// Use convenience methods from AsyncCommands trait
// Note: connection must be mutable here too
let _: () = connection.set("async_key", "Hello Async Redis!").await?;
println!("SET async_key");

let value: String = connection.get("async_key").await?;
println!("GET async_key: {}", value);

let new_count: i32 = connection.incr("async_counter", 1).await?;
println!("INCR async_counter: new value is {}", new_count);

6.4 Complete Asynchronous Example

Modify src/main.rs for async operations:

use redis::aio::Connection;
use redis::{AsyncCommands, Client, RedisResult}; // Import AsyncCommands
use anyhow::Result;

// Async connect function from above
async fn connect_async() -> RedisResult {
    let redis_url = "redis://127.0.0.1:6379/";
    let client = Client::open(redis_url)?;
    let connection = client.get_async_connection().await?;
    println!("Asynchronously connected to Redis!");
    Ok(connection)
}

// Async function to run Redis commands
async fn run_redis_commands_async(connection: &mut Connection) -> RedisResult<()> {
    // SET command
    let _: () = connection.set("async_key", "Value set asynchronously").await?;
    println!("SET async_key");

    // GET command
    let value: String = connection.get("async_key").await?;
    println!("GET async_key: {}", value);

    // INCR command
    let new_count: i32 = connection.incr("async_counter", 1).await?;
    println!("INCR async_counter: new value is {}", new_count);

    Ok(())
}

#[tokio::main]
async fn main() -> Result<()> { // Use anyhow::Result for convenience
    println!("Running asynchronous Redis example...");
    match connect_async().await {
        Ok(mut conn) => { // Connection needs to be mutable
            if let Err(e) = run_redis_commands_async(&mut conn).await {
                eprintln!("Error running async Redis commands: {}", e);
                // Convert redis::RedisError to anyhow::Error if needed, or handle specifically
                return Err(e.into());
            }
        }
        Err(e) => {
            eprintln!("Error connecting async to Redis: {}", e);
            return Err(e.into()); // Propagate error as anyhow::Error
        }
    }
    Ok(())
}

Run this with cargo run.

7. Error Handling

Most redis-rs functions return redis::RedisResult, which is an alias for Result. Handle these errors using:

  • ? operator (within functions returning Result).
  • match statements to handle specific redis::ErrorKind values.
  • unwrap() or expect() (use cautiously, as they panic on error).
  • Error handling crates like anyhow or thiserror for more structured error management.

8. Conclusion

The redis-rs crate provides a comprehensive and flexible interface for interacting with Redis from Rust. Whether you need simple synchronous operations or high-performance asynchronous communication using Tokio, the library offers a clear API based on standard Redis commands. Remember to handle potential errors appropriately using Rust's Result type and consider the asynchronous API for non-blocking applications like web services.

External Resources