Celery : décharge tes tâches lentes en arrière-plan et libère ton API

Celery : décharge tes tâches lentes en arrière-plan et libère ton API

De quoi avez-vous besoin

Version de Python

3.x

Packages

  • {"nom": "celery", "version": "5.6.3"}
  • {"nom": "redis", "version": "8.1.0"}
  • {"nom": "httpx", "version": "0.28"}

Difficulté

Intermédiaire

Quand ton API appelle un modèle d'IA, génère une image ou construit un PDF, l'utilisateur attend. Dix secondes, trente secondes, parfois une minute. Pendant tout ce temps, la requête HTTP reste ouverte, ton serveur est occupé, et ton visiteur est à deux doigts de fermer l'onglet. La solution ne s'appelle ni patience ni multithreading bricolé : elle s'appelle Celery. Une file d'attente de tâches qui découple ton API de tout ce qui prend du temps. Tu réponds immédiatement, un worker s'occupe du travail dans son coin, et tu récupères le résultat quand il est prêt.

Celery est l'outil standard de Python pour les tâches en arrière-plan. Il n'exécute rien lui-même : il orchestre. Ton code dépose une tâche dans une file, des workers la consomment, et tout le monde reste fluide. On va monter ça ensemble, avec du vrai code.

Étape 1 — Installe Celery et son broker

Celery a besoin d'un broker : un intermédiaire qui stocke les tâches en attendant qu'un worker les récupère. Le plus courant et le plus simple, c'est Redis.

bash
pip install celery redis

Redis joue deux rôles ici : broker (la file d'attente) et backend (l'endroit où sont stockés les résultats des tâches). Un seul service, deux usages.

Étape 2 — Écris ta première tâche

Crée un fichier tasks.py :

python
from celery import Celery

app = Celery(
    "iancr",
    broker="redis://localhost:6379/0",
    backend="redis://localhost:6379/0",
)


@app.task
def additionner(a, b):
    return a + b

Le décorateur @app.task transforme une fonction ordinaire en tâche distribuable. Une fois enregistrée, elle peut partir dans la file et être exécutée par n'importe quel worker, sur n'importe quelle machine.

Étape 3 — Lance un worker

Dans un second terminal :

bash
celery -A tasks worker --loglevel=info

Le worker démarre et liste les tâches qu'il connaît. C'est un processus séparé qui attend du travail. Important : le worker et ton script ne partagent rien d'autre que Redis. C'est ça, le découplage.

Étape 4 — Déclenche la tâche

De retour dans ton script Python :

python
from tasks import additionner

resultat = additionner.delay(3, 4)
print(resultat.id)              # l'identifiant unique de la tâche
print(resultat.get(timeout=10))  # 7

.delay() envoie la tâche dans la file et revient immédiatement. Le calcul n'est pas fait au moment de l'appel : il est mis en attente. .get() bloque jusqu'au résultat, avec un timeout pour ne jamais rester pendu.

Étape 5 — Le vrai cas d'usage : un appel IA capricieux

Là où Celery brille, c'est sur les tâches lentes et fragiles. Un appel à une API d'IA peut durer vingt secondes, ou échouer sur un rate limit. Voici une tâche qui interroge un modèle avec trois tentatives automatiques :

python
import httpx
from celery import Celery

app = Celery("iancr", broker="redis://localhost:6379/0", backend="redis://localhost:6379/0")


@app.task(bind=True, max_retries=3, default_retry_delay=5)
def resumer(self, texte: str) -> str:
    try:
        reponse = httpx.post(
            "https://api.exemple.com/resumer",
            json={"texte": texte},
            timeout=60,
        )
        reponse.raise_for_status()
        return reponse.json()["resume"]
    except Exception as exc:
        raise self.retry(exc=exc)

bind=True donne accès à self, et donc à self.retry(). En cas d'échec, la tâche retourne dans la file et réessaie cinq secondes plus tard, jusqu'à trois fois. Ton appel fragile devient résilient, sans une seule boucle écrite à la main.

Étape 6 — Réponds tout de suite côté API

Le bénéfice se voit quand on branche ça sur une API. Avec FastAPI, tu réponds en quelques millisecondes au lieu de faire patienter trente secondes :

python
from fastapi import FastAPI
from tasks import resumer

app = FastAPI()


@app.post("/resumer")
def resumer_endpoint(texte: str):
    tache = resumer.delay(texte)
    return {"task_id": tache.id, "status": "en cours"}

L'utilisateur reçoit un identifiant de tâche instantanément. La requête ne traîne plus : le gros du travail est parti ailleurs.

Étape 7 — Récupère le résultat

Ton frontend, ou l'utilisateur, revient plus tard chercher le résultat :

python
from celery.result import AsyncResult
from tasks import app

def statut(task_id: str):
    res = AsyncResult(task_id, app=app)
    if res.state == "SUCCESS":
        return {"status": "terminé", "resultat": res.result}
    return {"status": res.state}

PENDING, STARTED, SUCCESS, FAILURE : tu connais l'état de chaque tâche à tout moment, et le résultat est disponible dès qu'il est prêt.

Étape 8 — Enchaîne les tâches

Une tâche peut alimenter la suivante. Résumer un texte, le traduire, l'envoyer par email : trois étapes, une seule ligne.

python
from celery import chain
from tasks import resumer, traduire, envoyer_email

flux = chain(resumer.s(texte) | traduire.s("anglais") | envoyer_email.s("moi@exemple.fr"))
flux()

La signature .s() prépare la tâche sans l'exécuter. Le pipe enchaîne : la sortie de l'une devient l'entrée de la suivante. traduire et envoyer_email sont deux autres tâches @app.task, définies sur le même modèle. Tu viens de construire un mini-pipeline, sans orchestrateur maison.

Celery ne rend pas ton code plus rapide. Il rend ton API plus réactive en déplaçant la lenteur ailleurs : dans des workers qui encaissent le travail pendant que l'utilisateur vaque à ses occupations. C'est la différence entre une barre de chargement qui n'en finit pas et une réponse immédiate. Une fois qu'on a goûté au .delay(), on ne revient plus aux appels bloquants.

K

Auteur

Krugz

Développeur fullstack

Développeur fullstack qui tente, tant bien que mal, de suivre l'actualité de l'IA au milieu d'une marée d'informations imbuvables et des milliers d'outils et de modèles qui sortent chaque jour.