How to Connect to MySQL from Rust

7 min readMySQL

Rust's MySQL story centers on one author's pair of crates: mysql (synchronous) and mysql_async (tokio-based), both pure-Rust implementations of the wire protocol sharing the same core (mysql_common). On top of those sit the usual toolkit choices: sqlx with its compile-time checked queries, and Diesel, whose MySQL backend binds to the C client library. This guide shows working code for each and covers what actually breaks: authentication plugins, the named-parameter translation quirk, pooling, and TLS.

The options

LibraryModelWhen to use itVersion (as of July 2026)
mysqlSync driver, pure Rust, pool includedScripts, services without async28.0.0
mysql_asyncAsync driver (tokio), pure Rust, pool includedAsync services0.37.0
sqlxAsync toolkit, pure RustCompile-time checked SQL0.9.0
DieselSync ORM + query builderSchema-first apps, type-safe DSL2.3.11

Unlike the PostgreSQL ecosystem, both dedicated drivers ship a connection pool -- no separate pooling crate needed.

The mysql crate: synchronous

[dependencies]
mysql = "28"
use mysql::*;
use mysql::prelude::*;
 
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let url = "mysql://app:secret@localhost:3306/mydb";
    let pool = Pool::new(url)?;          // pool, not a single connection
    let mut conn = pool.get_conn()?;
 
    let names: Vec<String> = conn.query("SELECT name FROM users LIMIT 5")?;
    println!("{names:?}");
    Ok(())
}

Pool::new is the normal entry point even for small programs; a standalone Conn::new(url) exists if you genuinely want one connection.

Parameterized queries

Prepared statements use ? positional placeholders, and they are the only way to pass Rust values to the server -- there's no client-side string interpolation API, which is the right default:

let selected: Vec<(i64, String)> = conn.exec(
    "SELECT id, name FROM users WHERE region = ? AND active = ?",
    ("emea", true),
)?;

Named parameters are supported with :name syntax via the params! macro:

conn.exec_drop(
    "INSERT INTO payments (customer_id, amount) VALUES (:customer_id, :amount)",
    params! { "customer_id" => 42, "amount" => 1999 },
)?;

The naming trap: MySQL itself has no named parameters -- the crate translates them client-side, and parameter names must match [_a-z][_a-z0-9]*. Camel case silently breaks: :fooBar is parsed as parameter :foo followed by literal text Bar, so the statement becomes ?Bar and fails confusingly. Keep placeholder names lowercase with underscores.

One more limitation shared by all these drivers: you can't bind a vector to WHERE id IN ?. Build the (?, ?, ?) list to match the vector length, or use a different query shape.

Batch inserts

conn.exec_batch(
    "INSERT INTO payments (customer_id, amount) VALUES (:customer_id, :amount)",
    payments.iter().map(|p| params! {
        "customer_id" => p.customer_id,
        "amount" => p.amount,
    }),
)?;

exec_batch prepares once and executes per row -- much faster than looping exec_drop.

mysql_async: the tokio driver

[dependencies]
mysql_async = "0.37"
tokio = { version = "1", features = ["full"] }
use mysql_async::prelude::*;
 
#[tokio::main]
async fn main() -> Result<(), mysql_async::Error> {
    let pool = mysql_async::Pool::new("mysql://app:secret@localhost:3306/mydb");
    let mut conn = pool.get_conn().await?;
 
    let names: Vec<String> = conn
        .exec("SELECT name FROM users WHERE region = ?", ("emea",))
        .await?;
 
    drop(conn);                 // returns the connection to the pool
    pool.disconnect().await?;   // pool shutdown is explicit and async
    Ok(())
}

Two lifecycle details that differ from what you might expect:

  • Pool is a smart pointer -- clones share one pool. Create it once, clone it into handlers.
  • The pool must be disconnected explicitly with pool.disconnect().await before the program exits. Dropping it doesn't run async cleanup; skipping disconnect can lose in-flight cleanup and leaves server-side sessions to time out on their own.

Query syntax, ? placeholders, :name named parameters (same lowercase rule), and exec_batch all mirror the sync crate.

Authentication: caching_sha2_password

Both crates implement mysql_native_password (pre-8.0 default) and caching_sha2_password (default since MySQL 8.0). That matters because:

  • MySQL 8.x: works out of the box. Full authentication with caching_sha2_password requires either TLS or an RSA key exchange; the drivers handle this, but if you've disabled TLS and the server can't serve its RSA public key, auth fails.
  • MySQL 9.x: mysql_native_password is removed server-side. Old tutorials that tell you to ALTER USER ... IDENTIFIED WITH mysql_native_password as a fix no longer work there -- and you don't need it with these drivers.
  • MariaDB: uses its own defaults (never caching_sha2_password); standard password auth works, but accounts using MariaDB's ed25519 plugin won't authenticate from these drivers.

TLS

Both crates offer two backends behind feature flags: native-tls (OS TLS stack) and rustls (pure Rust, in aws-lc-rs or ring flavors). In mysql_async, native-tls-tls is the commonly used default option. One documented rustls caveat applies to both: rustls requires a hostname -- connecting by raw IP address fails certificate validation by design.

mysql_async = { version = "0.37", features = ["native-tls-tls"] }

Then request TLS in the URL: mysql://app:secret@db.example.com/mydb?require_ssl=true (see the Opts/SslOpts docs for CA pinning and client certs).

sqlx with MySQL

Same crate as for PostgreSQL, different feature flag:

sqlx = { version = "0.9", features = ["runtime-tokio", "tls-rustls-ring-webpki", "mysql"] }
use sqlx::mysql::MySqlPoolOptions;
 
let pool = MySqlPoolOptions::new()
    .max_connections(10)
    .connect("mysql://app:secret@localhost/mydb")
    .await?;
 
let user = sqlx::query!("SELECT id, name FROM users WHERE id = ?", 42_i64)
    .fetch_one(&pool)
    .await?;

The query! macro checks SQL against a live database at compile time (DATABASE_URL), or against committed metadata from cargo sqlx prepare in CI. Placeholders are ? for MySQL, $1 for PostgreSQL -- sqlx uses each database's native syntax rather than abstracting it.

Diesel with MySQL

Diesel's MySQL backend links against libmysqlclient via mysqlclient-sys -- the one option here with a C build dependency:

diesel = { version = "2.3", features = ["mysql"] }

You need the client library headers installed (default-libmysqlclient-dev on Debian/Ubuntu, mysql-client via Homebrew), or use the mysqlclient-src feature to build and statically link it from source. Connection and queries look the same as Diesel's PostgreSQL usage, with MysqlConnection::establish(&url).

diesel-async (0.9.2) provides an async MySQL connection built on mysql_async -- pure Rust, no libmysqlclient. SeaORM (2.0.0) is the async ORM alternative, built on sqlx.

Charset: use utf8mb4

MySQL's legacy utf8 charset is a 3-bytes-per-character subset that rejects emoji and some CJK characters. The drivers default sensibly, but your tables are the usual problem -- create them with CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci (MySQL 8+). An Incorrect string value: '\xF0\x9F...' error means a utf8 column, not a driver bug.

Common errors

ErrorCauseFix
ERROR 1045 (28000): Access deniedWrong credentials, or host-pattern account mismatchCheck user, password, and that the account allows your client host ('app'@'%' vs 'app'@'localhost')
Auth fails only with TLS disabled (MySQL 8)caching_sha2_password full auth needs TLS or RSA exchangeEnable TLS, or ensure the server can serve its RSA public key
Unknown authentication plugin 'auth_ed25519'MariaDB ed25519 accountUse a standard-password account for Rust clients
:fooBar parameter errors or wrong SQLCamel case in named parametersLowercase + underscores only: :foo_bar
Incorrect string value: '\xF0...'Legacy utf8 columnConvert to utf8mb4
rustls: certificate error connecting by IPrustls requires hostname verificationConnect by hostname, or use native-tls
Hangs at exit (mysql_async)Pool never disconnectedpool.disconnect().await? before exit
Too many connectionsPool size × replicas exceeds max_connections (default 151)Do the multiplication; raise max_connections deliberately, not reflexively

Which one to pick

  • Sync service or CLI: the mysql crate -- batteries included, pool built in.
  • Async service, plain SQL: mysql_async, same API in tokio form.
  • You want SQL verified at build time: sqlx.
  • Type-safe query builder + migrations: Diesel (accept the C dependency or use mysqlclient-src), or diesel-async to skip it.

Once connected, you still need to explore the data. Mako connects to MySQL with AI-powered autocomplete -- handy for prototyping the queries you're about to freeze into params! calls. Try it free at mako.ai.

Mako — open source

Skip the terminal. Use Mako.

Connect your database, write queries with AI assistance, and import/export data in clicks. Free to start.