Package: fastapi-async-safe-dependencies
FastAPI Async Safe DependenciesInstallationpipinstallfastapi-async-safe-dependenciesIntroductionFastAPI is a great framework for building APIs and the main purpose of this library is to make it a little bit better.
I guess someone (as I do) has a lot of class-based dependencies in your project.
But FastAPI comes with one little problem that will make your application slower than it could be.Let's take a look at the following example from FastAPI documentation:fromfastapiimportDepends,FastAPIapp=FastAPI()fake_items_db=[{"item_name":"Foo"},{"item_name":"Bar"},{"item_name":"Baz"},]classCommonQueryParams:def__init__(self,q:str|None=None,skip:int=0,limit:int=100,)->None:self.q=qself.skip=skipself.limit=limit@app.get("/items/")asyncdefread_items(commons:CommonQueryParams=Depends()):response={}ifcommons.q:response.update({"q":commons.q})items=fake_items_db[commons.skip:commons.skip+commons.limit]response.update({"items":items})returnresponseThis example is pretty simple and it works fine, but it has one little problem.
Whenever FastAPI tries to resolveCommonQueryParamsdependency it will delegate object creation to
the thread-pool executor, which is not good for performance. It will happen with every request.FastAPI logic is pretty simple, ifasyncio.iscoroutinefunctionreturnsTruefor the dependency, it will be called
as a simple coroutine, otherwise, it will be delegated to a thread-pool executor.FastAPI is usinganyio.to_thread.run_syncfunction to delegate object creation to the thread-pool executor.anyiothread-pool executor has a limited number of threads (40 by default) which means that if you have more than 40
requests at the same time, some of them might be blocked until one of the threads from the pool is released.
Thread will be blocked by simple class instantiation, this is not CPU intensive operation,
that is not the type of task you want to delegate to the thread-pool executor.This library provides a simple solution for this problem, that will avoid unnecessary thread-pool executor usage
for class-based dependencies and it should improve your application performance.UsageLet's take a look at the same example, but withfastapi-async-safe-dependencieslibrary:fromfastapiimportDepends,FastAPIfromfastapi_async_safeimportasync_safe,init_appapp=FastAPI()init_app(app)# don't forget to initialize applicationfake_items_db=[{"item_name":"Foo"},{"item_name":"Bar"},{"item_name":"Baz"},]@async_safe# you just need to add this decorator to your dependencyclassCommonQueryParams:def__init__(self,q:str|None=None,skip:int=0,limit:int=100,)->None:self.q=qself.skip=skipself.limit=limit@app.get("/items/")asyncdefread_items(commons:CommonQueryParams=Depends()):response={}ifcommons.q:response.update({"q":commons.q})items=fake_items_db[commons.skip:commons.skip+commons.limit]response.update({"items":items})returnresponseThat's it, now your dependency will be called as simple coroutine, and it will not be delegated to the thread-pool.Code above is equivalent to the following code:fromfastapiimportDepends,FastAPIapp=FastAPI()fake_items_db=[{"item_name":"Foo"},{"item_name":"Bar"},{"item_name":"Baz"},]classCommonQueryParams:def__init__(self,q:str|None=None,skip:int=0,limit:int=100,)->None:self.q=qself.skip=skipself.limit=limitasyncdef_common_query_params(q:str|None=None,skip:int=0,limit:int=100,)->CommonQueryParams:returnCommonQueryParams(q=q,skip=skip,limit=limit)@app.get("/items/")asyncdefread_items(commons:CommonQueryParams=Depends(_common_query_params)):response={}ifcommons.q:response.update({"q":commons.q})items=fake_items_db[commons.skip:commons.skip+commons.limit]response.update({"items":items})returnresponseDocumentationAll you need to do is to add@async_safedecorator to your dependency class or synchronous function that
should not be delegated to the thread-pool executor.fromtypingimportAnyfromdataclassesimportdataclassfromfastapi_async_safeimportasync_safe@dataclass@async_safeclassCommonQueryParams:q:str|None=Noneskip:int=0limit:int=100@async_safedefcommon_query_params(q:str|None=None,skip:int=0,limit:int=100,)->dict[str,Any]:return{"q":q,"skip":skip,"limit":limit}BothCommonQueryParamsclass andcommon_query_paramsfunction will not be delegated to the thread-pool executor.If your class inherits from a class that is decorated with@async_safedecorator then this class will beasync-safetoo.fromdataclassesimportdataclassfromfastapi_async_safeimportasync_safe@dataclass@async_safeclassBaseQueryParams:q:str|None=None@dataclass# this class will be async-safe tooclassCommonQueryParams(BaseQueryParams):skip:int=0limit:int=100If you don't want your inherited class to beasync-safeyou can use@async_unsafedecorator.fromdataclassesimportdataclassfromfastapi_async_safeimportasync_safe,async_unsafe@dataclass@async_safeclassBaseQueryParams:q:str|None=None@dataclass@async_unsafe# this class no longer will be async-safeclassCommonQueryParams(BaseQueryParams):skip:int=0limit:int=100Also, don't forget to initialize your application withinit_appfunction, otherwise,@async_safedecorator will not
have any effect.init_appfunction will monkey-patchDependantinstances on application startup.fromfastapiimportFastAPIfromfastapi_async_safeimportinit_appapp=FastAPI()init_app(app)# don't forget to initialize application !!!If you want to wrap all your class-based dependencies with@async_safedecorator you can passall_classes_safe=Trueargument toinit_appfunction. It will wrap all your class-based dependencies expect those that are decorated with@async_unsafedecorator.fromfastapiimportFastAPIfromfastapi_async_safeimportinit_appapp=FastAPI()init_app(app,all_classes_safe=True)BenchmarksPlease take a look at thebenchmarkdirectory for more details.Performance boost depends on the number of class-based dependencies in your application, the more dependencies you have,
the more performance boost you will get.