backend / fastapi
Background tasks
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.
| Besoin | BackgroundTasks | File 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 retry | Adapté |
| Survie à un redémarrage du serveur | Non | Oui, 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
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()# 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}# 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()# 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é
BackgroundTasksexé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
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.