asyncmy
August 12, 2026 ยท View on GitHub
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.
Features
- ๐ 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)
Benchmark
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
๐ View detailed benchmarks โ
Install
Requirements: Python โฅ 3.9
pip install asyncmy
Windows
asyncmy 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.
Usage
connect
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())
Prepared statements (binary protocol)
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.
Pool
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())
Type checking
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.
Rotating credentials
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.
Statement logging
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.
Replication
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())
Acknowledgments
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)