How to use rustls crate in Rust TLS

Enable rustls in Rust by adding the rustls-tls feature to your reqwest dependency in Cargo.toml.

When OpenSSL gets in the way

You are building a CLI tool that fetches configuration from a remote server. You drop reqwest into your Cargo.toml, write a few lines to get the JSON, and it works perfectly on your laptop. Then you try to build for a minimal Alpine container, or a platform that forbids dynamic linking, and the build fails. The default TLS backend needs OpenSSL headers and libraries that just aren't there. Or maybe you are auditing your dependencies and realize your app pulls in a massive C library just to handle HTTPS. You need a TLS implementation that lives entirely in Rust, compiles everywhere, and doesn't drag in C dependencies.

What rustls actually is

rustls is a TLS library written entirely in Rust. Most HTTP clients in Rust default to native-tls, which wraps the operating system's TLS library. On Linux, that is usually OpenSSL. OpenSSL is written in C. It is battle-tested, but it requires C headers to compile, it links dynamically by default, and it introduces a dependency on a codebase you didn't write and can't audit as easily.

rustls replaces that C layer. It implements the TLS protocol from scratch in safe Rust. No FFI. No C dependencies. It compiles to any target that Rust supports, including WebAssembly, and it enforces memory safety guarantees that C libraries simply cannot provide. The code is reviewed, the types are checked, and the borrow checker ensures no use-after-free bugs can slip into the handshake logic.

Rust doesn't trust C. rustls proves you don't have to either.

Switching reqwest to rustls

The most common way to use rustls is through reqwest. reqwest supports multiple TLS backends via feature flags. The default backend is native-tls. Switching to rustls requires two steps: disabling the default features and enabling the rustls-tls feature.

[dependencies]
# Disable default features to remove native-tls.
# Enable rustls-tls to use the pure Rust backend.
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] }
/// Fetches data using reqwest with the rustls backend.
async fn fetch_data() -> Result<(), Box<dyn std::error::Error>> {
    // Create a client builder.
    // The rustls feature is active, so this client uses rustls automatically.
    let client = reqwest::Client::new();

    // Perform the request.
    // rustls handles certificate verification and encryption.
    let response = client.get("https://httpbin.org/get").send().await?;
    let text = response.text().await?;

    println!("Response: {}", text);
    Ok(())
}

The key here is default-features = false. reqwest enables native-tls by default. If you just add rustls-tls without disabling defaults, you end up with both backends compiled into your binary. That bloats your build and wastes space. The convention is to disable defaults and pick exactly what you need.

Disable defaults. Pick your backend. Keep your binary lean.

How the handshake works

When you compile, cargo fetches rustls and its dependencies. Since everything is Rust, the compiler checks types and memory safety. No cc crate invoking gcc or clang. No missing openssl-dev packages. The build is reproducible across platforms.

At runtime, when you call client.get(), reqwest delegates the connection setup to rustls. rustls performs the TLS handshake, verifies the server's certificate against the trust store, and establishes the encrypted channel. If the certificate is expired or the hostname doesn't match, rustls rejects the connection. You get an error, not a silent downgrade.

rustls prefers TLS 1.3. It falls back to TLS 1.2 if the server doesn't support 1.3. It disables TLS 1.0 and 1.1 entirely. It also disables weak ciphers by default. You don't have to configure this. The defaults are secure. OpenSSL often defaults to allowing older protocols for compatibility, which introduces risk. rustls forces you to be modern.

Trust the defaults. They are safer than you think.

Handling certificate roots

rustls does not bundle root certificates by default. This keeps the crate size down and avoids shipping a static list of certs that might become outdated. Instead, rustls relies on the caller to provide roots. reqwest handles this via features.

In a standard desktop environment, you can use rustls-native-certs. This feature tells rustls to load the system's certificate store. On Linux, it reads from /etc/ssl/certs. On macOS, it uses the Keychain. On Windows, it uses the Windows certificate store.

[dependencies]
# Use native-certs to load system roots.
# Good for desktop apps where the OS manages certificates.
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "rustls-native-certs"] }

In minimal environments like Alpine containers or WebAssembly, the system might not have a root certificate store. You need webpki-roots. This feature bundles a curated set of root certificates from Mozilla. It ensures your app can verify certificates even in an empty container.

[dependencies]
# Use webpki-roots for environments without system certs.
# Essential for minimal containers and reproducible builds.
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "webpki-roots"] }

The convention is clear. Use webpki-roots for containers and reproducible builds. Use rustls-native-certs for desktop applications that should respect the user's system configuration. If you see a certificate verification error in a clean container, check your roots, not your code.

Check your roots before you blame the protocol.

Customizing rustls behavior

reqwest exposes enough configuration to cover most use cases. You can set timeouts, disable verification for testing, or add client certificates.

use reqwest::Client;
use std::time::Duration;

/// Builds a client with custom timeouts and rustls.
fn build_configured_client() -> Client {
    Client::builder()
        // Set a timeout for the entire request.
        .timeout(Duration::from_secs(10))
        // Build the client.
        // rustls is used because of the Cargo.toml feature flag.
        .build()
        .expect("Failed to build client")
}

For local development, you might encounter self-signed certificates. reqwest allows you to disable verification, but this is dangerous.

/// Builds a client that skips certificate verification.
/// WARNING: Only use this for local development or testing.
/// Never use this in production.
fn build_insecure_client() -> Client {
    Client::builder()
        // Disable certificate verification.
        // This makes the connection vulnerable to MITM attacks.
        .danger_accept_invalid_certs(true)
        .build()
        .expect("Client build failed")
}

If you disable verification, you own the risk. The compiler won't stop you, but the network will expose you. Use this only when you control both ends of the connection and understand the implications.

If you disable verification, you own the risk.

Pitfalls to avoid

The biggest trap is forgetting default-features = false. You add rustls-tls, compile, and wonder why your binary is larger than expected. You have both OpenSSL and rustls linked. The compiler won't stop you. It just includes both. Check your Cargo.toml and ensure defaults are off.

Another trap is running in a minimal environment without roots. rustls cannot verify certificates if it has no roots to check against. If you deploy to Alpine and forget webpki-roots, every HTTPS request fails with a certificate error. Add the feature or load roots manually.

Performance is rarely an issue. rustls is fast. It uses efficient algorithms and avoids unnecessary allocations. In benchmarks, it often matches or exceeds OpenSSL for TLS 1.3. If you see performance problems, profile your application. The TLS layer is rarely the bottleneck.

Bloat is silent. Check your features.

Choosing your TLS backend

Use rustls when you need a pure Rust stack with no C dependencies. Use rustls when you are targeting WebAssembly or exotic architectures where OpenSSL isn't available. Use rustls when you want to audit your TLS implementation or enforce memory safety across the entire stack. Use rustls when you are building for minimal containers and want reproducible builds without system packages.

Use native-tls when you are building a desktop application that should leverage the OS's native certificate store and TLS preferences for maximum compatibility with local system settings. Use native-tls when your deployment environment already has OpenSSL installed and you want to minimize binary size by linking against the system library rather than bundling TLS code. Use native-tls when you need to support legacy protocols that rustls has dropped, though you should question why you need legacy protocols.

Pick the backend that matches your deployment reality, not your comfort zone.

Where to go next