Background Tasks in FastAPI

Before jumping into Celery. Let's start with the most straightforward tool to help us understand background tasks. FastAPI already has a BackgroundTasks class that can help us implement simple background tasks.

Let's create a virtual environment to isolate our project requirements.

python -m venv env

Now, all we need is FastAPI and a web server e.g. Uvicorn or Hypercorn. Before installing these let's first activate our virtualenv. In Linux, you can use source ./env/bin/activate

Now, we are all set to install our requirements, type the below command:

pip install uvicorn==0.21.1
pip install fastapi==0.95.0

We can now proceed with the code. I am having the below folder structure:

├─📄 background_tasks.py
└─📁 env/

Type out the below code in the file background_tasks.py

import time
from fastapi import FastAPI

app = FastAPI()


def send_push_notification(device_token: str):
    time.sleep(10)  # simulates slow network call to firebase/sns
    with open("notification.log", mode="a") as notification_log:
        response = f"Successfully sent push notification to {device_token}\n"
        notification_log.write(response)


@app.get("/push/{device_token}")
async def notify(device_token: str):
    send_push_notification(device_token)
    return {"message": "Notification sent"}

The above example tries to simulate the sending of a push notification. Instead of sending a real push notification, We are going to log the recipient device token to a file.

Let's start the uvicorn server to test out our work. To start the server use: uvicorn background_tasks:app --reload

Now, try visiting http://127.0.0.1:8000/push/1. Did you observe something 🐠y? The page took around 10 seconds to load. Exactly!! It's because of the 10 seconds delay that we have manually added to simulate network requests for push notification service. This is not the ideal behavior. We are unnecessarily making the end user wait for something that they don't need to.

Here are some experiments that I suggest to understand synchronous tasks better.

  • Open 3-4 browser tabs. Reload each tab quickly and check for logs in notification.log file.
  • Notice how much time each tab is taking.
  • Notice the total time of completion of all the tabs.

There are 2 major problems that we can observe:

  1. Request is blocking i.e. it is taking 10 seconds to notify the users that their request has been fulfilled.
  2. If we send more than 1 requests. They are not being processed parallely.

Problem number 2 can be mitigated to some extent. Try running uvicorn background_tasks:app --workers 2 Caution:  You can not use the --reload flag with workers defined. Now, let's again try to repeat our experiments. This time you should notice that if you open 2 browser tabs and reload them quickly then the total time should be around 10 seconds only instead of 20 seconds. However, if you open 4 tabs and reload then it will still take 20 seconds. Not a wonderful solution but at least there is some progress.

To improve this system, We can throw away the sending push notification logic to the background. We can ask FastAPI to procrastinate the push notification logging part. To achieve this, We can use the BackgroundTasks class.

import time
from fastapi import BackgroundTasks, FastAPI

app = FastAPI()


def send_push_notification(device_token: str):
    time.sleep(15)  # simulates slow network call to firebase/sns
    with open("notification.log", mode="a") as notification_log:
        response = f"Successfully sent push notification to {device_token}\n"
        notification_log.write(response)


@app.get("/push/{device_token}")
async def notify(device_token: str, background_tasks :  BackgroundTasks):
    background_tasks.add_task(send_push_notification,device_token)
    return {"message": "Notification sent"}

Again hit the same URL endpoint: http://127.0.0.1:8000/push/1 however this time you should see that our request is not blocked for 10 seconds. We get the response as soon as we hit the URL endpoint. The background_tasks object has a method add_task() which receives the following arguments(in order):

  • A function/callable to be run in the background
  • the sequence of arguments.
  • the sequence of keyword arguments.

When FastAPI encounters background_tasks.add_task(send_push_notification, device_token), It knows that it's time to procrastinate and run the write_notifation function at some later point in time. This time it took around 10ms in locally!!
 

If you visit the log file after 10-12 seconds, You should see that the write was successful. Isn't this awesome, now we can speed up our average response time by multi folds without compromising on the quality of our service. Secondly, even if you reload 4 web pages quickly or via docs, the logging for them will appear around the same time in approx 10 seconds. This is because the tasks are not executed sequentially rather in a threadpool.

BackgroundTasks class is good, however, it has its own limitations. As of now, It is not having some advanced features like retrying tasks, task orchestration, tracking the task, or revoking it. The other major limitation is when we are performing some super heavy computation, we might not want the task to be run in the same process. Sometimes we might want to distribute tasks over multiple servers. In such cases, Celery is one of the best alternatives. We will start exploring it in the next chapter.

FastAPITutorial

Brige the gap between Tutorial hell and Industry. We want to bring in the culture of Clean Code, Test Driven Development.

We know, we might make it hard for you but definitely worth the efforts.

Contacts

Refunds:

Refund Policy
Social

Follow us on our social media channels to stay updated.

© Copyright 2022-23 Team FastAPITutorial