Asynchronous Database Operations in FastAPI with SQLModel and Tortoise ORM
Ethan Miller
Product Engineer · Leapcell

Introduction
In the rapidly evolving landscape of backend development, building high-performance, scalable web services is paramount. Asynchronous programming has emerged as a cornerstone for achieving such goals, allowing applications to handle numerous concurrent requests without blocking the main thread. FastAPI, with its inherent support for asynchronous operations and intuitive design, has quickly become a go-to framework for building modern APIs. However, the benefits of asynchronous web frameworks can be significantly hampered if database interactions remain synchronous. This bottleneck is precisely what asynchronous ORMs and database libraries aim to solve. This article will explore how to integrate two prominent asynchronous database tools – SQLModel and Tortoise ORM – with FastAPI to unlock truly non-blocking database operations, thereby enhancing the overall efficiency and responsiveness of your backend services.
Core Concepts and Principles
Before diving into the implementation details, let's clarify some fundamental terms crucial for understanding asynchronous database operations in FastAPI.
- Asynchronous Programming: A programming paradigm that allows a program to execute a long-running task without freezing the entire application. In Python, this is primarily achieved using
async
/await
keywords and theasyncio
library, enabling cooperative multitasking. - ORM (Object-Relational Mapping): A technique that lets developers interact with a database using an object-oriented paradigm. Instead of writing raw SQL queries, ORMs allow you to manipulate database records as objects in your preferred programming language. This abstraction simplifies database operations, improves code readability, and often enhances security by preventing SQL injection.
- FastAPI: A modern, fast (high-performance) web framework for building APIs with Python 3.7+ based on standard Python type hints. It is deeply integrated with
asyncio
, making it ideal for asynchronous web services. - SQLModel: A Python library for interacting with SQL databases, designed to be both "SQL and Python first." It's built on top of Pydantic and SQLAlchemy, aiming to provide a simpler, more intuitive experience for defining models that double as both Pydantic models for data validation and SQLAlchemy models for database interaction. It inherently supports asynchronous operations through SQLAlchemy's async engine.
- Tortoise ORM: An easy-to-use asynchronous ORM for Python, specifically designed for
asyncio
anduvloop
. It provides a simple API for defining models, performing queries, and managing database migrations, all while maintaining an asynchronous nature.
The principle behind using asynchronous ORMs with FastAPI is straightforward: when your FastAPI application needs to communicate with a database, instead of waiting synchronously for the database response, the ORM allows the application to switch to another task. Once the database operation is complete, the application can resume processing the original request. This non-blocking behavior is critical in I/O-bound operations like database calls, preventing the server from becoming unresponsive under heavy load.
Implementing Asynchronous Database Operations
Let's explore how to integrate SQLModel and Tortoise ORM into a FastAPI application for asynchronous database interactions. We'll use a simple "Hero" model for demonstration.
Using SQLModel
SQLModel blends Pydantic models with SQLAlchemy, offering a powerful and elegant way to define your data.
Setup
First, install the necessary packages:
pip install fastapi "uvicorn[standard]" sqlmodel "psycopg2-binary" # or asyncpg for async
We'll use PostgreSQL as our database. For asynchronous operations with PostgreSQL, asyncpg
is often preferred over psycopg2-binary
, but psycopg2-binary
can be used with an async wrapper. Let's stick with asyncpg
for a native async experience.
pip install fastapi "uvicorn[standard]" sqlmodel asyncpg
Database Configuration and Model Definition
from typing import Optional, List from sqlmodel import Field, SQLModel, Session, create_engine from contextlib import asynccontextmanager from fastapi import FastAPI, Depends, HTTPException, status # Database URL, adjust as needed DATABASE_URL = "postgresql+asyncpg://user:password@host:port/dbname" class HeroBase(SQLModel): name: str = Field(index=True) secret_name: str age: Optional[int] = Field(default=None, index=True) class Hero(HeroBase, table=True): id: Optional[int] = Field(default=None, primary_key=True) class HeroCreate(HeroBase): pass class HeroPublic(HeroBase): id: int # Asynchronous engine engine = create_engine(DATABASE_URL, echo=True) async def create_db_and_tables(): async with engine.begin() as conn: await conn.run_sync(SQLModel.metadata.create_all) # Dependency to get a database session async def get_session(): async with Session(engine) as session: yield session # FastAPI application setup @asynccontextmanager async def lifespan(app: FastAPI): await create_db_and_tables() yield app = FastAPI(lifespan=lifespan)
FastAPI Endpoints
Now, let's create some basic CRUD endpoints for the Hero
model.
@app.post("/heroes/", response_model=HeroPublic) async def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): db_hero = Hero.model_validate(hero) session.add(db_hero) await session.commit() await session.refresh(db_hero) return db_hero @app.get("/heroes/", response_model=List[HeroPublic]) async def read_heroes(offset: int = 0, limit: int = Field(default=100, le=100), session: Session = Depends(get_session)): heroes = (await session.exec(select(Hero).offset(offset).limit(limit))).all() return heroes @app.get("/heroes/{hero_id}", response_model=HeroPublic) async def read_hero(*, session: Session = Depends(get_session), hero_id: int): hero = (await session.get(Hero, hero_id)) if not hero: raise HTTPException(status_code=404, detail="Hero not found") return hero @app.put("/heroes/{hero_id}", response_model=HeroPublic) async def update_hero(*, session: Session = Depends(get_session), hero_id: int, hero: HeroCreate): db_hero = (await session.get(Hero, hero_id)) if not db_hero: raise HTTPException(status_code=404, detail="Hero not found") hero_data = hero.model_dump(exclude_unset=True) for key, value in hero_data.items(): setattr(db_hero, key, value) session.add(db_hero) await session.commit() await session.refresh(db_hero) return db_hero @app.delete("/heroes/{hero_id}") async def delete_hero(*, session: Session = Depends(get_session), hero_id: int): hero = (await session.get(Hero, hero_id)) if not hero: raise HTTPException(status_code=404, detail="Hero not found") await session.delete(hero) await session.commit() return {"ok": True}
The key aspects here are:
create_engine
withpostgresql+asyncpg
for an asynchronous driver.async with engine.begin()
andawait conn.run_sync()
for creating tables asynchronously.async with Session(engine) as session:
for managing asynchronous database sessions.await session.exec()
andawait session.get()
for asynchronous queries.await session.commit()
andawait session.refresh()
for saving changes and refreshing objects.
Using Tortoise ORM
Tortoise ORM is designed from the ground up for asynchronous operations and provides a more traditional ORM experience with its own query syntax.
Setup
Install Tortoise ORM and your chosen async database driver (e.g., asyncpg
for PostgreSQL).
pip install fastapi "uvicorn[standard]" tortoise-orm asyncpg
Database Configuration and Model Definition
from typing import Optional, List from fastapi import FastAPI, HTTPException, status from pydantic import BaseModel from tortoise import fields, models from tortoise.contrib.fastapi import register_tortoise from tortoise.exceptions import DoesNotExist # Database configuration TORTOISE_CONFIG = { "connections": {"default": "postgresql://user:password@host:port/dbname"}, "apps": { "models": { "models": ["main"], # Assuming models are in this file "default_connection": "default", } }, } # Tortoise ORM Hero Model class Hero(models.Model): id = fields.IntField(pk=True) name = fields.CharField(max_length=255, unique=True, index=True) secret_name = fields.CharField(max_length=255) age = fields.IntField(null=True, index=True) class Meta: table = "heroes" def __str__(self): return self.name # Pydantic models for request/response validation class HeroIn(BaseModel): name: str secret_name: str age: Optional[int] = None class HeroOut(BaseModel): id: int name: str secret_name: str age: Optional[int] = None async def init_db(app: FastAPI): register_tortoise( app, config=TORTOISE_CONFIG, generate_schemas=True, # Will create tables if they don't exist add_exception_handlers=True, ) app = FastAPI() # Register Tortoise ORM during app startup @app.on_event("startup") async def startup_event(): await init_db(app)
FastAPI Endpoints
@app.post("/heroes/", response_model=HeroOut) async def create_hero(hero_in: HeroIn): hero = await Hero.create(**hero_in.model_dump()) return await HeroOut.from_tortoise_orm(hero) @app.get("/heroes/", response_model=List[HeroOut]) async def get_heroes(offset: int = 0, limit: int = 100): heroes = await Hero.all().offset(offset).limit(limit) return [await HeroOut.from_tortoise_orm(hero) for hero in heroes] @app.get("/heroes/{hero_id}", response_model=HeroOut) async def get_hero(hero_id: int): try: hero = await Hero.get(id=hero_id) return await HeroOut.from_tortoise_orm(hero) except DoesNotExist: raise HTTPException(status_code=404, detail="Hero not found") @app.put("/heroes/{hero_id}", response_model=HeroOut) async def update_hero(hero_id: int, hero_in: HeroIn): try: hero = await Hero.get(id=hero_id) await hero.update_from_dict(hero_in.model_dump(exclude_unset=True)) await hero.save() return await HeroOut.from_tortoise_orm(hero) except DoesNotExist: raise HTTPException(status_code=404, detail="Hero not found") @app.delete("/heroes/{hero_id}", status_code=204) async def delete_hero(hero_id: int): try: hero = await Hero.get(id=hero_id) await hero.delete() return {"message": "Hero deleted successfully"} except DoesNotExist: raise HTTPException(status_code=404, detail="Hero not found")
With Tortoise ORM:
register_tortoise
handles database connection and schema generation.- Models inherit from
models.Model
and usefields
for defining attributes. await Hero.create()
,await Hero.all()
,await Hero.get()
,await hero.save()
,await hero.delete()
are all asynchronous operations.HeroOut.from_tortoise_orm()
is a convenient method provided bytortoise.contrib.pydantic
to convert an ORM instance to a Pydantic model.
Application Scenarios
Both SQLModel and Tortoise ORM excel in scenarios where:
- High Concurrency is Expected: Large numbers of users or requests necessitate efficient I/O handling.
- Microservices Architectures: Decoupled services often benefit from fast, non-blocking communication with their respective databases.
- Real-time Applications: APIs serving real-time data or updates require low latency database operations.
- Modern Python Backends: Integrating with frameworks like FastAPI naturally leverages their async capabilities.
SQLModel's strengths lie in its tight integration with Pydantic, making it excellent for projects where data validation and serialization are crucial and you want a single source of truth for your data models. It's built on SQLAlchemy's robust foundation, offering advanced query capabilities.
Tortoise ORM's strengths include its simpler API, built explicitly for asyncio
, and often a gentler learning curve for newcomers to async ORMs. It's particularly appealing if you prefer a more standalone ORM solution with its own distinct syntax for queries.
Conclusion
Integrating asynchronous database operations into your FastAPI applications using ORMs like SQLModel or Tortoise ORM is a critical step towards building performant and scalable web services. Both tools provide robust, asynchronous capabilities that eliminate I/O bottlenecks, ensuring your application remains responsive even under heavy load. SQLModel offers a unified model definition with Pydantic and SQLAlchemy's power, while Tortoise ORM provides a concise, asyncio
-native experience. Choosing between them often comes down to specific project requirements, team familiarity, and preference for their respective API styles, but either will significantly elevate your FastAPI application's database interaction efficiency. By embracing asynchronous ORMs, you can fully leverage FastAPI's capabilities and deliver truly non-blocking, high-performance backends.