How to Connect to PostgreSQL from Rust (sqlx, diesel)

Connect to PostgreSQL in Rust using sqlx or diesel by adding dependencies and initializing a connection pool with your database URL.

When data lives elsewhere

You're building a service that needs to remember things. A user signs up, a transaction happens, a log gets written. You need a database. PostgreSQL is the workhorse for a reason. You've got your Rust code, you've got your database, and now you need a bridge. The bridge isn't just a string of SQL; it's a connection pool, type safety, and a way to handle the fact that network I/O blocks threads. You reach for a crate. You find sqlx and diesel. They look similar. They solve the same problem. They approach it differently.

The bridge between Rust and Postgres

Rust gives you two main ways to talk to a database. sqlx is like a spell-checker that runs while you type. It reads your SQL, checks it against the schema, and refuses to compile if there's a typo or a type mismatch. diesel is like a blueprint generator. You define your tables in Rust code, and it generates the SQL for you. Both keep you safe. Both have trade-offs.

The database is a remote machine speaking a specific protocol. You can't just shout a request and hope for the best. You need a library card (connection), you need to follow the rules (SQL syntax), and you need to know exactly what book you're getting back (types). Rust takes this further. The compiler wants to check your request before you even walk into the library.

Minimal connection

Start with sqlx. It's the most common choice for new projects because it lets you write SQL directly while getting compile-time guarantees. You need a Cargo.toml with the right features and a main.rs that sets up a connection pool.

# Cargo.toml
[dependencies]
# runtime-tokio-rustls binds sqlx to the tokio async runtime and uses rustls for TLS.
# postgres enables the PostgreSQL driver.
sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "postgres"] }
tokio = { version = "1", features = ["full"] }
// src/main.rs
use sqlx::postgres::PgPoolOptions;

#[tokio::main]
async fn main() {
    // Load URL from env var. Convention is DATABASE_URL.
    let db_url = std::env::var("DATABASE_URL")
        .unwrap_or_else(|_| "postgres://postgres:password@localhost".to_string());

    // PgPoolOptions configures the connection pool.
    // max_connections sets the upper limit of open connections.
    let pool = PgPoolOptions::new()
        .max_connections(5)
        .connect(&db_url)
        .await
        .expect("Failed to connect to database");

    // query_scalar runs a query that returns a single value.
    // fetch_one expects exactly one row.
    let row: String = sqlx::query_scalar("SELECT 'hello'")
        .fetch_one(&pool)
        .await
        .expect("Query failed");

    println!("{row}");
}

The runtime-tokio-rustls feature flag does two jobs. It binds sqlx to the tokio async runtime, and it selects rustls for TLS encryption. rustls is a pure-Rust implementation of TLS. This matters because it removes the dependency on the system's OpenSSL library. When you deploy your binary, you don't need to worry about missing libssl on the target machine. The binary carries its own encryption stack.

Don't open a connection for every query. Pool them, or your latency will spike.

What happens under the hood

When you call connect, sqlx performs a handshake with the PostgreSQL server. It negotiates the protocol version, authenticates using the credentials in the URL, and establishes an encrypted channel if TLS is enabled. The PgPool then keeps this connection alive and ready to use.

A connection pool is a cache of open connections. Opening a TCP connection involves a handshake, authentication, and protocol negotiation. Doing this for every query adds latency. The pool opens a set of connections upfront and keeps them alive. When your code needs to run a query, it borrows a connection from the pool, runs the query, and returns the connection. If the pool is exhausted, new requests wait until a connection becomes available. You tune this with max_connections and acquire_timeout.

The #[tokio::main] macro sets up an async runtime. Rust doesn't have a global event loop. You need a runtime to manage asynchronous tasks. tokio is the most popular runtime. The async fn and await keywords tell the compiler to yield control back to the runtime while waiting for I/O. This allows your program to handle many requests concurrently without blocking threads.

If you forget to await the connection, you get a Future instead of a PgPool. The compiler rejects this with E0308 (mismatched types) because you're trying to assign a future to a concrete type.

Trust the pool. It handles the hard work of connection management.

Realistic query with types

Real code doesn't just select strings. It fetches rows and maps them to structs. sqlx provides the FromRow derive macro to handle this mapping.

use sqlx::{FromRow, PgPool};

#[derive(Debug, FromRow)]
struct User {
    // Column names must match the database schema exactly.
    // Types must be compatible with PostgreSQL types.
    id: i64,
    name: String,
    email: String,
}

async fn get_user(pool: &PgPool, id: i64) -> Result<Option<User>, sqlx::Error> {
    // query_as! checks the SQL against the schema at compile time.
    // $1 is a parameterized placeholder. Never interpolate strings.
    sqlx::query_as!(User, "SELECT id, name, email FROM users WHERE id = $1", id)
        .fetch_optional(pool)
        .await
}

The query_as! macro does more than map types. If you have the DATABASE_URL set, sqlx can connect to the database at compile time and verify that the SQL is valid. It checks that the table exists, the columns exist, and the types match. If you rename a column in the database, your Rust code fails to compile. This catches schema drift before it reaches production.

You can also run cargo sqlx prepare to cache the schema. This allows builds without a live database connection. The cache is stored in .sqlx/ and committed to version control. This is a community convention for projects using sqlx's compile-time checking.

Parameterize your queries. String interpolation is a security hole, and the compiler won't stop you unless you use the macro.

Pitfalls and compiler errors

Database code has sharp edges. Nulls are the biggest one. If a column can be null, your Rust type must be an Option. If you map a nullable column to a non-nullable type, sqlx returns a runtime error when it encounters a null.

#[derive(Debug, FromRow)]
struct User {
    id: i64,
    // If the email column allows NULL, this must be Option<String>.
    email: Option<String>,
}

If you try to use a type that doesn't implement Decode, the compiler rejects you with E0277 (trait bound not satisfied). For example, if you try to map a PostgreSQL TEXT column to a Vec<u8>, you'll see an error like the trait bound Vec<u8>: sqlx::Type<postgres::Postgres> is not satisfied. You need to use a type that sqlx knows how to decode, like String.

Connection pools are shared resources. If you try to move the pool out of a function, you might run into E0382 (use of moved value). The convention is to pass a reference to the pool or clone it. PgPool implements Clone, and cloning is cheap. It just increments a reference count. You can clone the pool and pass it to different parts of your application.

// Cloning the pool is cheap and safe.
let pool_clone = pool.clone();
tokio::spawn(async move {
    // Use pool_clone here.
});

If you try to move a connection out of the pool without returning it, you leak the connection. The pool tracks connections using guards. When the guard is dropped, the connection is returned to the pool. Don't fight the guard. Let it do its job.

Handle nulls explicitly. The database doesn't lie, but it does return nulls.

Choosing your tool

Rust has several database libraries. The right choice depends on your workflow and your mental model.

Use sqlx when you want compile-time checked SQL. Use sqlx when you write raw SQL but need the safety net of schema validation at build time. Use sqlx when you need a lightweight crate with minimal abstraction over the database protocol.

Use diesel when you prefer a query builder that constructs SQL from Rust expressions. Use diesel when you want an ORM-style workflow with schema definitions in code and automatic migration management. Use diesel when you need complex associations and eager loading built into the library.

Use the postgres crate when you need a raw, low-level driver for building your own abstraction layer. Use the postgres crate when you are implementing a higher-level library and don't want the overhead of sqlx or diesel.

Pick the tool that matches your mental model. SQL-first or Rust-first. Both get you to safety.

Where to go next