Fastapi 0.100.0-beta1: even faster
Beta version released last week FastAPI 0.100-beta1, which means what? That’s right, it’s time for performance tests!
Changes
The main change in the new version of FastAPI is the transition to the new version of the Pydantic v2.0b3 library – all validation logic has been rewritten in Rust. Pydantic is promised a 5-50x performance boost! Well, let’s see how this will affect the speed of FastAPI in general. Other changes in version 0.100-beta1 release notes not indicated.
For Pydantic
Test bench preparation
We are web developers Notwe love CRUD, so let’s test it on it. To at least try to get closer to a real application, for each client request, it will work with the SQLAlchemy model, accessing the database.
All core code is available at githubHere are the main points:
The model will have three date-time fields, two text fields, one field bool
and of course id
:
Model code
# commons/models.py
from datetime import datetime
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy.types import Text
from sqlalchemy.sql import func
class Base(DeclarativeBase):
pass
class Post(Base):
__tablename__ = "posts"
id: Mapped[int] = mapped_column(primary_key=True)
created_at: Mapped[datetime] = mapped_column(server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(onupdate=func.now())
published_at: Mapped[datetime] = mapped_column(nullable=False)
title: Mapped[str] = mapped_column(Text)
content: Mapped[str] = mapped_column(Text)
is_deleted: Mapped[bool] = mapped_column(nullable=False, default=False)
What? Haven’t seen the new-stylish-young style of SQLAlchemy called mapping_styles
? Then rather to documentation. In general, changes were made again so that our favorite IDEs would not swear when we try to write some data not Column, but for example int, str and so on, into an attribute of an object of type Column.
The Pydantic v1 schema is the standard Pydantic model:
laugh code
# commons/schemas.py
from datetime import datetime
from enum import StrEnum
from pydantic import BaseModel, validator
class PostOut(BaseModel):
id: int
published_at: datetime
updated_at: datetime
title: str
content: str
is_published: bool | None = None
@validator("is_published", always=True)
def compute_is_published(cls, v, values, **kwargs):
return datetime.utcnow() >= values["published_at"]
class Config:
orm_mode = True
class PostsOut(BaseModel):
posts: list[PostOut]
class PostIn(BaseModel):
title: str
content: str
published_at: datetime
class Order(StrEnum):
ASC = "asc"
DESC = "desc"
Of the interesting here – only field calculations is_published
“on the fly”, that is – when returning to the client.
For tests, we will make three endpoints, one of them is for writing posts to the database, the other is for reading posts from the database, the third is a purely synthetic speed test:
Routing code
# api/posts/router.py
from fastapi import APIRouter, Depends, Response, status, HTTPException
from sqlalchemy.orm import Session
from commons.database import get_db
from commons import schemas
from crud import posts
router = APIRouter(tags=["posts"])
@router.get("/posts")
def get_posts(
per_page: int = 10,
page: int = 0,
order: schemas.Order = schemas.Order.DESC,
session: Session = Depends(get_db),
) -> schemas.PostsOut:
return schemas.PostsOut(posts=posts.get(per_page, per_page*page, order, session))
@router.get("/posts_synthetic")
def posts_synthetic(
per_page: int = 10,
) -> schemas.PostsOut:
return schemas.PostsOut(
posts=[
schemas.PostOut(
id=i,
published_at=datetime(2023, 6, 30, 12, 0, 0),
updated_at=datetime(2023, 6, 30, 12, 0, 0),
title="Статья",
content="Съешь ещё этих мягких французских булок, да выпей же чаю.",
)
for i in range(per_page)
]
)
@router.post("/posts")
def create_post(
post_in: schemas.PostIn, session: Session = Depends(get_db)
) -> schemas.PostOut:
post = posts.create(post_in, session)
return post
In full accordance with the documentation, I give the work of converting models from SQLAlchemy into the final response to the client to FastAPI.
I reduced the operations for working with the database to a minimum, without updating and deleting:
Code for working with the database
# crud/posts.py
from datetime import datetime
from typing import Sequence
from sqlalchemy import insert, select, update, desc, asc
from sqlalchemy.orm import Session
from sqlalchemy import exc
from commons.schemas import PostIn, Order
from commons.models import Post
def get(limit: int, offset: int, order: Order, session: Session) -> Sequence[Post]:
q = (
select(Post)
.where(Post.is_deleted == False)
.order_by(
desc(Post.published_at) if order is Order.DESC else asc(Post.published_at)
)
.limit(limit)
.offset(offset)
)
return session.execute(q).scalars().all()
def create(post_in: PostIn, session: Session) -> Post:
q = (
insert(Post)
.values(
updated_at=datetime.utcnow(),
published_at=post_in.published_at,
title=post_in.title,
content=post_in.content,
is_deleted=False,
)
.returning(Post)
)
post = session.execute(q).scalar_one()
session.commit()
return post
Changes when migrating to Pydantic 2
Changes to the major version bring changes to the interfaces, so our version of the application running on FastAPI 0.100.0-beta1 + Pydantic 2 will also need changes. Scrolling fast Migration Guidefor my test application I had to make the following changes:
Update dependencies. Here a surprise awaited me – it turns out that in the version of Pydantic 2 they decided to take out familiar to many
BaseSettings
to a separate librarypydantic-settings
! And she demands in dependenciestyping-extensions<4.0.0
when the new version of alchemy 2.0.17 requirestyping-extensions>=4.2.0
… It’s good that there is only one variable in my little CRUD, so we putos.getenv
and forgotten – but in large applications it can steal a lot of nerves.In Pydantic model configuration
orm_mode
works but warns that the title has changed tofrom_attributes
. We change.always=True in the Pydantic model now does not work, but the long-awaited decorator has appeared
computed_field
– now the computed property looks much more decent:
class PostOut(BaseModel):
...
@computed_field
@property
def is_published(self) -> bool:
return datetime.utcnow() >= self.published_at
In general, the transition on a small application looks painless.
Performance Testing
And now let’s move on to the cherry on the cake – the tests themselves. For this I wrote a script test.sh
which:
starts the database, starts the application, tests the client using the utility
ab
(Apache benchmark) for application on FastAPI version 0.98.0demolishes everything with docker compose down -v
repeats the first point for the application on FastAPI version 0.100-beta1
The requests themselves are 1000 POST entries /posts and 1000 reads of the first 100 posts GET /posts?per_page=100, the number of simultaneously executed requests (parameter c
) = 10
Since I am not a respected bash master, my output from the script is somewhat clumsy, but I will give you the already processed data (everywhere I took the average of the three runs performed by the test):
fastapi 0.98.0 | fastapi 0.100.0-beta1 | |
---|---|---|
READ r/s | 126.90 | 371.19 |
READ r/s synthetic | 172.57 | 1203.18 |
WRITE r/s | 342.11 | 352.65 |
MEM USAGE BEFORE | 72.44MiB | 85.95MiB |
MEM USAGE AFTER | 85.95MiB | 98.91MiB |
Conclusions:
The main thing is that the speed of return of the first 100 posts has increased by x2.92 times! It just helps that the speed of accessing the database does not play a role so much with a large number of repetitive requests. But the speed of the framework has a strong influence.
With a purely synthetic test without accessing the database, the speed increased by ~7 times!
The write speed, which mainly depends on the speed of the database, has increased, but not significantly.
But you have to pay for everything – the consumption of RAM has increased by about 15%.
Conclusion
I was pleased with the large increase in FastAPI speed. Pydantic V2 is currently on github open 7 issue4 of which are about static code analyzers, in general – not much.
If you want to run additional tests – it’s very easy to fork the repository and make changes, so welcome!
Application source code:
github.com