Retour au cours

backend / fastapi

Background tasks

Leçon 151 exercice

Explication

Ce que vous allez apprendre

  • Exécuter du code après l'envoi de la réponse HTTP avec BackgroundTasks
  • Enchaîner plusieurs tâches en arrière-plan dans un ordre garanti
  • Utiliser une tâche async pour un appel réseau non bloquant
  • Identifier les limites de BackgroundTasks (pas de persistance, pas de retry)
  • Savoir quand basculer vers une vraie file de tâches comme Celery ou Arq

Dans quel contexte ?

L'endpoint POST /users de app/routers/users.py doit créer un utilisateur en base ET lui envoyer un email de bienvenue via un service SMTP externe qui répond parfois en 3 secondes. Faire attendre le client ces 3 secondes avant de recevoir la confirmation de création dégraderait inutilement l'expérience, alors que l'email n'est pas critique pour la réponse. Déplacer l'envoi de l'email dans une BackgroundTasks permet de renvoyer la réponse immédiatement après la création en base, l'email partant juste après.

Le problème, d'abord

Certaines actions déclenchées par une requête n'ont pas besoin d'être terminées avant que le client reçoive sa réponse. Envoyer un email de bienvenue ou notifier un service tiers, par exemple.

Faire attendre le client pour ces opérations annexes dégraderait inutilement le temps de réponse perçu. Alors que le travail "essentiel", comme créer l'utilisateur en base, est déjà terminé.

BackgroundTasks répond exactement à ce besoin. En ajoutant une tâche via background_tasks.add_task(...), FastAPI attend que la réponse HTTP soit ENVOYÉE au client, puis exécute la fonction fournie.

Cette exécution se fait dans le MÊME process serveur, sans bloquer la connexion. Le client n'a donc aucune idée du temps que prend réellement cette tâche annexe, il a déjà sa réponse.

Une fois ce mécanisme compris, il faut connaître ses limites avant de l'utiliser en production. BackgroundTasks est un outil volontairement simple.

Si le serveur redémarre ou plante entre l'envoi de la réponse et l'exécution de la tâche, celle-ci est PERDUE. Sans aucune notification, et sans mécanisme de nouvelle tentative automatique en cas d'échec.

C'est donc adapté à des tâches non critiques et rapides, comme un log ou un email "best effort". Mais pas à une tâche dont l'échec aurait un impact business réel, comme facturer un client.

BesoinBackgroundTasksFile de tâches (Celery/Arq)
Tâche courte, non critique (log, email best-effort)AdaptéExcessif
Tâche critique (facturation, traitement de paiement)Risqué : pas de retryAdapté
Survie à un redémarrage du serveurNonOui, persistée dans le broker

Piège fréquent

Si le serveur redémarre ou plante entre l'envoi de la réponse et l'exécution de la tâche, celle-ci est PERDUE, sans aucune notification ni retry automatique. Ne confie jamais une opération critique (facturation, envoi de contrat) à une simple BackgroundTasks.

Pour aller plus loin, sache qu'il existe une solution plus robuste pour ces cas critiques. Quand une tâche est critique, longue, ou doit survivre à un redémarrage du serveur, il faut une vraie file de tâches comme Celery ou Arq.

Elle repose sur un broker externe (Redis, RabbitMQ) qui persiste les tâches en attente et gère les retries. Un sujet volontairement laissé hors du périmètre de cette leçon d'introduction, mais essentiel à connaître pour la suite.

Commandes & code

Background tasks

python
from fastapi import FastAPI, BackgroundTasks

app = FastAPI()


def send_welcome_email(email: str):
    # exécuté APRÈS que la réponse HTTP a déjà été envoyée au client
    print(f"Envoi de l'email de bienvenue à {email}")
    # appel réel à un service SMTP ici


@app.post("/users")
def create_user(email: str, background_tasks: BackgroundTasks):
    user = save_user_to_db(email)

    background_tasks.add_task(send_welcome_email, email)
    # la réponse est renvoyée immédiatement, sans attendre l'envoi de l'email

    return {"id": user.id, "email": email}


def save_user_to_db(email: str):
    class U:
        id = 1
    return U()
python
# Plusieurs tâches enchaînées, exécutées dans l'ordre d'ajout
def log_signup(email: str):
    print(f"Log : inscription de {email}")


def notify_admin(email: str):
    print(f"Notification admin : nouvel utilisateur {email}")


@app.post("/v2/users")
def create_user_v2(email: str, background_tasks: BackgroundTasks):
    user = save_user_to_db(email)

    background_tasks.add_task(log_signup, email)
    background_tasks.add_task(send_welcome_email, email)
    background_tasks.add_task(notify_admin, email)

    return {"id": user.id}
python
# Background task async — utile pour des appels I/O (API externe, DB)
import httpx


async def notify_slack(message: str):
    async with httpx.AsyncClient() as client:
        await client.post(
            "https://hooks.slack.com/services/XXX",
            json={"text": message},
        )


@app.post("/orders")
async def create_order(product_id: int, background_tasks: BackgroundTasks):
    order = create_order_in_db(product_id)
    background_tasks.add_task(notify_slack, f"Nouvelle commande #{order.id}")
    return order


def create_order_in_db(product_id: int):
    class O:
        id = 42
    return O()
python
# Limite importante : BackgroundTasks ne survit PAS à un redémarrage du serveur
# et ne fournit aucun mécanisme de retry. Pour une tâche critique/longue,
# préférer une vraie file de tâches (Celery, RQ, Arq) avec un broker (Redis/RabbitMQ).

# Exemple avec Arq — tâche persistée, avec retry automatique
# worker.py
async def send_invoice_email(ctx, order_id: int):
    order = await fetch_order(order_id)
    await send_email(order.customer_email, "invoice.html", order=order)


class WorkerSettings:
    functions = [send_invoice_email]
    max_tries = 3  # retry automatique en cas d'échec

# Depuis l'API :
# await redis_pool.enqueue_job("send_invoice_email", order_id=order.id)

Résumé

  • BackgroundTasks exécute du code après l'envoi de la réponse, sans faire attendre le client.
  • Les tâches s'exécutent dans le même process, dans l'ordre d'ajout via add_task.
  • Adapté aux tâches courtes et non critiques (email, log) — pas de garantie de retry ni de persistance.
  • Pour des tâches longues/critiques, préférer une vraie file de jobs (Celery, Arq, RQ) avec un broker dédié.

Exercices pratiques

1 disponible
1

Mission : les emails de facturation perdus lors d'un redémarrage

Objectif : Corriger un mauvais usage de BackgroundTasks pour une tâche critique et l'utiliser correctement pour une tâche non critique.

Contexte

POST /orders/{id}/invoice de app/routers/orders.py utilise background_tasks.add_task(send_invoice_email, order_id) pour envoyer la facture par email. Le service a redémarré trois fois cette semaine pour des déploiements, et une poignée de clients n'a jamais reçu sa facture, sans qu'aucune erreur n'apparaisse nulle part. Par ailleurs, POST /users bloque encore le client 3 secondes pour un email de bienvenue non critique.

Résoudre l’exercice →