Python asyncpg Database — Core Concepts

What asyncpg Is

asyncpg is a high-performance, asyncio-native PostgreSQL client library for Python. It was created by Yury Selivanov (also behind uvloop) and is used by projects like EdgeDB, Tortoise ORM, and many FastAPI applications.

Key differentiators:

  • Pure binary protocol — communicates with PostgreSQL using the binary wire protocol, not text
  • Asyncio native — built from scratch for async/await, not a wrapper around a sync driver
  • C-level performance — critical paths are written in Cython

Basic Usage

Single Connection

import asyncpg

async def main():
    conn = await asyncpg.connect(
        host='localhost',
        database='mydb',
        user='user',
        password='pass'
    )
    
    # Fetch multiple rows
    rows = await conn.fetch("SELECT id, name FROM users WHERE active = $1", True)
    for row in rows:
        print(row['id'], row['name'])  # Rows are dict-like Record objects
    
    # Fetch one row
    row = await conn.fetchrow("SELECT count(*) FROM users")
    
    # Fetch one value
    count = await conn.fetchval("SELECT count(*) FROM users")
    
    await conn.close()

Note the $1, $2 parameter syntax — asyncpg uses PostgreSQL’s native parameter format, not %s or ?.

Connection Pools

For production use, always use a pool:

async def main():
    pool = await asyncpg.create_pool(
        dsn='postgresql://user:pass@localhost/mydb',
        min_size=5,
        max_size=20
    )
    
    async with pool.acquire() as conn:
        users = await conn.fetch("SELECT * FROM users")
    
    await pool.close()

The pool manages connection lifecycle — creating new connections when needed, reusing idle ones, and enforcing limits.

Prepared Statements

asyncpg can prepare statements for repeated execution:

async with pool.acquire() as conn:
    stmt = await conn.prepare("SELECT * FROM users WHERE department = $1")
    
    # Execute the same query with different parameters
    engineering = await stmt.fetch('engineering')
    marketing = await stmt.fetch('marketing')

Prepared statements are parsed and planned once by PostgreSQL, saving overhead on repeated queries.

Transactions

async with pool.acquire() as conn:
    async with conn.transaction():
        await conn.execute(
            "INSERT INTO orders (user_id, amount) VALUES ($1, $2)",
            user_id, amount
        )
        await conn.execute(
            "UPDATE accounts SET balance = balance - $1 WHERE id = $2",
            amount, user_id
        )
    # Transaction commits automatically; rolls back on exception

For savepoints (nested transactions):

async with conn.transaction():
    await conn.execute("INSERT INTO log (msg) VALUES ($1)", "start")
    
    try:
        async with conn.transaction():  # Creates a savepoint
            await conn.execute("INSERT INTO risky_table VALUES ($1)", data)
    except asyncpg.UniqueViolationError:
        pass  # Savepoint rolled back, outer transaction continues
    
    await conn.execute("INSERT INTO log (msg) VALUES ($1)", "end")

Type Mapping

asyncpg automatically converts between PostgreSQL and Python types:

PostgreSQLPython
integer, bigintint
real, double precisionfloat
text, varcharstr
booleanbool
timestampdatetime.datetime
datedatetime.date
jsonbdict / list
uuiduuid.UUID
arraylist

For custom types, register codecs:

await conn.set_type_codec(
    'geometry',
    encoder=encode_geometry,
    decoder=decode_geometry,
    schema='public'
)

asyncpg vs psycopg (sync)

Featureasyncpgpsycopg2/3
Async supportNativepsycopg3 has async mode
ProtocolBinaryText (default)
Speed (single query)FastModerate
Speed (high concurrency)Much fasterLimited by threads
ORM integrationTortoise, encode/databasesSQLAlchemy, Django
EcosystemGrowingMature

Common Misconception

“asyncpg is always faster than psycopg2.” For a single query on a single connection, the speed difference is modest. asyncpg’s advantage appears under concurrency — when dozens or hundreds of queries are in flight simultaneously. A FastAPI endpoint serving 1,000 concurrent requests benefits enormously; a batch script running queries one at a time may see only marginal gains.

One thing to remember: asyncpg excels at high-concurrency PostgreSQL access — its binary protocol and native async design make it the fastest choice for web servers and applications that juggle many simultaneous database conversations. Use connection pools, use $1 parameter syntax, and let transactions handle your consistency needs.

pythonasyncasyncpgdatabasespostgresql

See Also

  • Python Aioredis Understand Aioredis through a practical analogy so your Python decisions become faster and clearer.
  • Python Alembic Understand Alembic through a practical analogy so your Python decisions become faster and clearer.
  • Python Asyncpg Understand Asyncpg through a practical analogy so your Python decisions become faster and clearer.
  • Python Cassandra Python Understand Cassandra Python through a practical analogy so your Python decisions become faster and clearer.
  • Python Connection Pooling Understand Connection Pooling through a practical analogy so your Python decisions become faster and clearer.