How to Connect to MySQL from Rust
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
| Library | Model | When to use it | Version (as of July 2026) |
|---|---|---|---|
mysql | Sync driver, pure Rust, pool included | Scripts, services without async | 28.0.0 |
mysql_async | Async driver (tokio), pure Rust, pool included | Async services | 0.37.0 |
sqlx | Async toolkit, pure Rust | Compile-time checked SQL | 0.9.0 |
| Diesel | Sync ORM + query builder | Schema-first apps, type-safe DSL | 2.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:
Poolis a smart pointer -- clones share one pool. Create it once, clone it into handlers.- The pool must be disconnected explicitly with
pool.disconnect().awaitbefore 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_passwordrequires 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_passwordis removed server-side. Old tutorials that tell you toALTER USER ... IDENTIFIED WITH mysql_native_passwordas 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'sed25519plugin 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
| Error | Cause | Fix |
|---|---|---|
ERROR 1045 (28000): Access denied | Wrong credentials, or host-pattern account mismatch | Check 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 exchange | Enable TLS, or ensure the server can serve its RSA public key |
Unknown authentication plugin 'auth_ed25519' | MariaDB ed25519 account | Use a standard-password account for Rust clients |
:fooBar parameter errors or wrong SQL | Camel case in named parameters | Lowercase + underscores only: :foo_bar |
Incorrect string value: '\xF0...' | Legacy utf8 column | Convert to utf8mb4 |
| rustls: certificate error connecting by IP | rustls requires hostname verification | Connect by hostname, or use native-tls |
| Hangs at exit (mysql_async) | Pool never disconnected | pool.disconnect().await? before exit |
Too many connections | Pool 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
mysqlcrate -- 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), ordiesel-asyncto 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.
Skip the terminal. Use Mako.
Connect your database, write queries with AI assistance, and import/export data in clicks. Free to start.