asyncmy is the fastest asyncio MySQL/MariaDB driver for Python. It keeps the familiar aiomysql API while rewriting the entire protocol core in Cython — down to pointer-level packet parsing. In our benchmarks it outperforms every driver tested, including the C-based synchronous mysqlclient.
- 🚀 Fastest in every benchmark — reads large result sets 2.1x faster than
mysqlclientand 5x faster thanaiomysql/pymysql(details) - 🔌 Drop-in aiomysql replacement — same API, same cursors (
DictCursor,SSCursor), same pool semantics - 🧬 Server-side prepared statements (binary protocol) via
conn.prepare()— no client-side escaping, no text parsing; large scans another ~35% faster than the text protocol - ⚡ C-speed protocol core — rows are parsed in bulk from the receive buffer in a single C loop, values decode straight from wire bytes via the CPython C-API
- 🏊 Built-in connection pool —
asyncmy.create_pool(), no extra dependency, 2x aiomysql's pooled throughput - 📡 MySQL replication protocol over asyncio (BinLogStream)
- ✅ CI-tested on MySQL and MariaDB (workflow)
asyncmy ranks #1 in all four scenarios against mysqlclient, pymysql, and aiomysql (warmup + best-of-3, see methodology):
| Test | asyncmy Rank | Performance |
|---|---|---|
| Large Result Set (33k rows, all types) | 🏆 #1/4 | 0.030s — 2.2x faster than mysqlclient, 5.3x faster than aiomysql |
| Connection Pool (2k queries) | 🏆 #1/2 | ~17,000 qps — 2x aiomysql's throughput |
| Concurrent Queries (50 connections) | 🏆 #1/2 | ~8,000 qps — 1.6x faster than aiomysql |
| Batch Insert (10k rows) | 🏆 #1/4 | ~107,000 rows/sec — fastest of all four drivers |
The protocol core is engineered for zero waste on the hot path:
- Bulk packet parsing: one socket read serves hundreds of row packets, parsed in a single C loop with no event-loop round-trips
- Pointer-based protocol reads: integers and length-encoded values are read directly from raw memory, no
structcalls - Direct row decoding: cell values decode straight from the receive buffer via the CPython C-API (
PyUnicode_DecodeUTF8,PyTuple_New), skipping intermediate objects - Zero-decode numeric/temporal columns:
int/float/datetimevalues parse directly from bytes, and dates are built with the C datetime API - Escape fast path: strings without special characters are returned as-is, no translation pass
Requirements: Python ≥ 3.9
pip install asyncmyasyncmy uses Cython extensions; on Windows you need Microsoft C++ Build Tools to build them.
-
Download Microsoft C++ Build Tools.
-
Open CMD as Administrator (recommended) and
cdto the folder where the installer was downloaded. -
Rename the installer (e.g.
vs_buildtools__XXXXXXXXX.XXXXXXXXXX.exe) tovs_buildtools.exefor convenience. -
Run (ensure ~5–6GB free disk space):
vs_buildtools.exe --norestart --passive --downloadThenInstall --includeRecommended --add Microsoft.VisualStudio.Workload.NativeDesktop --add Microsoft.VisualStudio.Workload.VCTools --add Microsoft.VisualStudio.Workload.MSBuildTools
-
Wait for installation to complete, then restart your computer.
-
Install asyncmy:
pip install asyncmy
You can uninstall the Build Tools afterward if desired.
Use asyncmy.connect() for a single connection. For many concurrent connections, use a connection pool.
import asyncio
import os
from asyncmy import connect
from asyncmy.cursors import DictCursor
async def main():
conn = await connect(
user=os.getenv("DB_USER"),
password=os.getenv("DB_PASSWORD", ""),
)
async with conn.cursor(cursor=DictCursor) as cursor:
await cursor.execute("CREATE DATABASE IF NOT EXISTS test")
await cursor.execute("""
CREATE TABLE IF NOT EXISTS test.`asyncmy` (
`id` int PRIMARY KEY AUTO_INCREMENT,
`decimal` decimal(10, 2),
`date` date,
`datetime` datetime,
`float` float,
`string` varchar(200),
`tinyint` tinyint
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
""".strip())
await conn.ensure_closed()
if __name__ == "__main__":
asyncio.run(main())For repeated queries, server-side prepared statements skip client-side escaping
entirely and read results in MySQL's binary protocol — numeric and temporal
columns decode natively with no text parsing. Placeholders use native ? syntax.
stmt = await conn.prepare("SELECT id, name FROM users WHERE id = ?")
result = await stmt.execute((42,))
print(result.rows) # tuple of row tuples
print(result.affected_rows) # for INSERT/UPDATE/DELETE
await stmt.close()
# or as a context manager
async with await conn.prepare("SELECT ? + ?") as stmt:
result = await stmt.execute((1, 2))Transparent mode: pass stmt_cache_size=N to connect()/create_pool() and
regular cursor.execute("... %s ...", args) calls automatically run as cached
server-side prepared statements — no code changes needed (ORMs benefit too).
Queries the server can't prepare fall back to the text protocol silently.
pool = await asyncmy.create_pool(stmt_cache_size=128, ...)Note: with the binary protocol, FLOAT columns return the exact stored value
rather than the text protocol's decimal-rounded rendering, which is why this
is opt-in.
For multiple connections, use a connection pool. Pass the same kwargs as connect() (e.g. host, user, password).
import asyncio
import asyncmy
async def main():
pool = await asyncmy.create_pool(host="localhost", user="root", password="")
async with pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute("SELECT 1")
ret = await cursor.fetchone()
assert ret == (1,)
pool.close()
await pool.wait_closed()
if __name__ == "__main__":
asyncio.run(main())asyncmy ships py.typed and stubs for its compiled modules, so mypy and Pylance resolve the API
without extra configuration:
conn = await asyncmy.connect(host="localhost", user="root") # -> Connection
async with conn.cursor() as cur:
rows = await cur.fetchall() # -> list[Any]Contributors: make stubs regenerates the stubs after changing a .pyx signature. make check
runs stubtest, which compares every stub against the compiled module and fails on drift.
Some credentials expire while a pooled connection outlives them — AWS RDS IAM auth tokens last 15
minutes, for instance. Pass password_creator instead of password and it is consulted before
every connection attempt, including the ones the pool makes on its own when it recycles or
reconnects:
import boto3
client = boto3.client("rds")
def rds_auth_token():
return client.generate_db_auth_token(
DBHostname="mydb.cluster.amazonaws.com",
Port=3306,
DBUsername="dbuser",
Region="us-east-1",
)
pool = await asyncmy.create_pool(
host="mydb.cluster.amazonaws.com",
user="dbuser",
password_creator=rds_auth_token,
)The callable may be a plain function or return an awaitable, and must return str or bytes. If
both password and password_creator are given, the creator wins.
echo=True logs every statement and its duration to the asyncmy logger at INFO level. Nothing
appears until logging is configured — logging.basicConfig(level=logging.INFO) at minimum, since
the root logger defaults to WARNING.
For anything beyond that, pass query_callback. It is called as callback(cursor, query, elapsed_ms) after every successful statement, with the duration as a float in milliseconds:
import logging
logger = logging.getLogger("myapp.sql")
def log_slow_queries(cursor, query, elapsed_ms):
if elapsed_ms > 100:
logger.warning("[%sms] %s", elapsed_ms, query)
conn = await asyncmy.connect(host="localhost", user="root", query_callback=log_slow_queries)executemany and callproc report once for the whole call rather than once per row. The callback
is independent of echo: set both and you get the log line and the callback.
asyncmy supports the MySQL replication protocol (like python-mysql-replication) over asyncio.
import asyncio
from asyncmy import connect
from asyncmy.replication import BinLogStream
async def main():
conn = await connect()
ctl_conn = await connect()
stream = BinLogStream(
conn,
ctl_conn,
server_id=1,
master_log_file="binlog.000172",
master_log_position=2235312,
resume_stream=True,
blocking=True,
)
async for event in stream:
print(event)
await conn.ensure_closed()
await ctl_conn.ensure_closed()
if __name__ == "__main__":
asyncio.run(main())asyncmy builds on these projects:
- PyMySQL — pure Python MySQL client
- aiomysql — asyncio MySQL driver
- python-mysql-replication — MySQL replication protocol (pure Python, on top of PyMySQL)