Better FastAPI Background Jobs

Better Background Tasks for FastAPI or Discord.py

The built-in background tasks from starlette are useful for running a job that you don’t want to make the API response wait for, but they lack some features I was looking for such as; repeated runs, point in time runs, and cron-like functionality

  • Pros
    • Runs after response is returned
    • Simply runs jobs in the same runtime
  • Cons
    • Didn’t support Cron or Future Scheduled jobs
    • No ability to persist jobs across restarts

Advanced Python Scheduler (APScheduler)

Eventually I found APScheduler, which seemed to offer a nice amount on functionality without the need to set up a separate runtime for the jobs

  • Pros
    • Simply runs jobs in the same runtime
    • Support Cron or Future Scheduled jobs
    • Ability to persist jobs across restarts
  • Cons
    • Doesn’t automatically wait until after response is returned (solved with workaround)
import logging
from datetime import datetime, timedelta

from apscheduler.schedulers.asyncio import AsyncIOScheduler
from fastapi import FastAPI

scheduler = AsyncIOScheduler()

logging.basicConfig(
    format='%(asctime)s - %(process)s - %(name)s:%(lineno)d - %(levelname)s -'
    ' %(message)s',
    level=logging.INFO,
)
logger = logging.getLogger(__name__)


# setup preconfigured jobs to run using the decorator
@scheduler.scheduled_job('interval', minutes=1)
async def example_heartbeat():
    now = datetime.now()
    logger.info(f'Time: {now}')


app = FastAPI()


# Start the scheduler running with a fastapi startup job
@app.on_event('startup')
async def startup_jobs():
    scheduler.start()


async def my_job(registered_ts):
    now_ts = datetime.now()
    logger.info(f"Job {registered_ts=}, {now_ts=}")


@app.get('/')
async def root():
    now = datetime.now()
    when = now + timedelta(minutes=5)
    # schedule adhoc jobs as needed
    scheduler.add_job(
        my_job, 'date', run_date=when, kwargs={'registered_ts': now}
    )

Alternate implementation, not running until after response

from fastapi import BackgroundTasks


@app.get('/')
async def root(background_tasks: BackgroundTasks):
    now = datetime.now()
    when = now + timedelta(minutes=5)
    # schedule adhoc jobs as needed (but after response returns)
    background_tasks.add_task(
        scheduler.add_job, my_job, 'date', run_date=when, kwargs={'registered_ts': now}
    )

What this enabled me to do with my Home Automation projects

  • Automatically switch of lights in my home
    • at particular times of the day
    • after a fixed delay since a motion sensor last activated or another trigger occurred
  • Periodically check if conditions had been met to send me alerts

Other options I considered

Repeated Tasks from fastapi-utils

  • Pros
    • Support for periodic jobs from startup (was sufficient for my initial use-cases)
    • Simply runs jobs in the same runtime
  • Cons
    • Didn’t support Cron or Future Scheduled jobs
    • No ability to persist jobs across restarts

Celery

Seemed a bit too heavy weight, requiring more complex configuration