Search code examples
python-asynciofastapiuvicorn

Using `async def` vs `def` in FastAPI and testing blocking calls


tl;dr

  1. Which of the options below is the correct workflow in fastapi?
  2. How does one programatically test whether a call is truly blocking (other than manually from browser)? Is there a stress testing extension to uvicorn or fastapi?

I have a number of endpoints in fastapi server (using uvicorn at the moment) that have long blocking calls to regular sync Python code. Despite the documentation (https://fastapi.tiangolo.com/async/) I am still unclear whether I should be using exclusively def, async def or mixing for my functions.

As far as I understand it, I have three options, assuming:

def some_long_running_sync_function():
  ...

Option 1 - consistently use def only for endpoints

@app.get("route/to/endpoint")
def endpoint_1:
  some_long_running_sync_function()


@app.post("route/to/another/endpoint")
def endpoint_2:
  ...

Option 2 - consistently use async def only and run blocking sync code in executor

import asyncio


@app.get("route/to/endpoint")
async def endpoint_1:
  loop = asyncio.get_event_loop()
  await loop.run_in_executor(None, some_long_running_sync_function)


@app.post("route/to/another/endpoint")
async def endpoint_2:
  ...

Option 3 - mix and match def and async def based on underlying calls

import asyncio


@app.get("route/to/endpoint")
def endpoint_1:
  # endpoint is calling to sync code that cannot be awaited
  some_long_running_sync_function()


@app.post("route/to/another/endpoint")
async def endpoint_2:
  # this code can be awaited so I can use async
  ...

Solution

  • Since no one is picking this up I‘m giving my two cents. So this stems only from my personal experience:

    Option 1

    This completely defeats the purpose of using an async framework. So, possible, but not useful.

    Option 2 and Option 3

    For me it‘s a mixture of both. The framework is explicitly made to support a mixture of sync and async endpoints. Generally I go with: No await in the function/CPU-bound task -> def, IO-bound/very short -> async.

    Pitfalls

    When you start using async programming it‘s easy to fall for the fallacy that now you don‘t have to care about thread-safety anymore. But once you start running stuff in threadpools this isn‘t true anymore. So remember that FastAPI is running your def endpoints in a threadpool and you are responsible to make them threadsafe.

    This also implies that you need to consider what you can and can‘t do with your event loop in def endpoints. Since your event loop is running in the main thread, asyncio.get_running_loop() will not work. This is why I sometimes define async def endpoints, even if there is no IO, to be able to access the event loop from the same thread. But of course then you have to keep your code short.

    As a side note: FastAPI also exposes starlettes backgroundtasks, which can be used as a dependency to create tasks from endpoints.

    As I‘m writing this, this seems pretty opinionated, which is probably a reason why you didn‘t get a lot of feedback until now. So feel free to shoot me down, if you disagree with anything :-)