Django Signals vs Celery Tasks en 2026 : Guide Complet et Questions d'Entretien
Découvrez quand utiliser les signaux Django ou les tâches Celery. Analyse approfondie du traitement synchrone vs asynchrone, implications de performance et questions d'entretien pour développeurs Django.

Les signaux Django et les tâches Celery permettent tous deux de gérer des événements dans les applications Django, mais ils résolvent des problèmes différents. Les signaux s'exécutent de manière synchrone dans le cycle requête-réponse, tandis que Celery délègue le travail à des workers en arrière-plan. Choisir le mauvais outil entraîne des réponses lentes, des conditions de concurrence ou une architecture inutilement complexe.
Utiliser les signaux Django pour les effets secondaires légers et synchrones qui doivent se terminer avant la réponse. Utiliser Celery pour tout ce qui prend plus de 100ms, implique des services externes, ou peut échouer indépendamment de la requête principale.
Fonctionnement Interne des Signaux Django
Les signaux Django implémentent le patron observateur. Lorsqu'un modèle effectue une sauvegarde, une suppression, ou quand une requête démarre ou se termine, Django émet un signal. Toute fonction connectée à ce signal s'exécute immédiatement, dans la même transaction de base de données et le même thread.
# signals.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.core.cache import cache
from .models import Product
@receiver(post_save, sender=Product)
def invalidate_product_cache(sender, instance, **kwargs):
# S'exécute de manière synchrone après Product.save()
cache_key = f"product:{instance.id}"
cache.delete(cache_key)
# Invalide également le listing de catégorie
cache.delete(f"category:{instance.category_id}:products")Le récepteur de signal s'exécute à l'intérieur de la même transaction de base de données. Si la transaction fait un rollback, les effets du gestionnaire de signal persistent. Cela a son importance pour l'invalidation du cache : le cache est vidé, mais le changement en base de données ne persiste jamais. Le résultat est un cache miss qui recharge des données obsolètes.
Django 5.2 a introduit transaction.on_commit() pour résoudre ce problème. Envelopper la logique du signal dans on_commit garantit l'exécution uniquement après un commit réussi :
# signals.py
from django.db import transaction
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Product
from .tasks import reindex_product
@receiver(post_save, sender=Product)
def handle_product_saved(sender, instance, **kwargs):
# Différer jusqu'à ce que la transaction soit validée
transaction.on_commit(
lambda: reindex_product.delay(instance.id)
)Ce patron fait le pont entre les signaux et Celery : le signal se déclenche de manière synchrone, mais le travail réel se produit de manière asynchrone après la validation de la transaction.
Modèle d'Exécution des Tâches Celery
Celery exécute les tâches dans des processus worker séparés. Une vue Django met en file d'attente une tâche en sérialisant ses arguments vers un broker de messages (Redis ou RabbitMQ). Un worker récupère le message et exécute la tâche indépendamment de la requête HTTP d'origine.
# tasks.py
from celery import shared_task
from django.core.mail import send_mail
from .models import Order
@shared_task(bind=True, max_retries=3, default_retry_delay=60)
def send_order_confirmation(self, order_id: int):
"""Envoie un email de confirmation après la commande."""
try:
order = Order.objects.select_related('user').get(id=order_id)
send_mail(
subject=f"Commande #{order.id} Confirmée",
message=f"Votre commande de {order.total} a été passée.",
from_email="commandes@example.com",
recipient_list=[order.user.email],
)
except Order.DoesNotExist:
# La commande a été supprimée avant l'exécution de la tâche
return
except Exception as exc:
# Réessayer en cas d'échecs transitoires (timeout SMTP, etc.)
raise self.retry(exc=exc)La tâche s'exécute dans un processus différent, potentiellement sur une machine différente. Elle n'a pas accès au contexte de la requête d'origine. Si la commande est supprimée entre la mise en file et l'exécution, la tâche doit gérer cela gracieusement.
Celery 5.4 (version stable actuelle en septembre 2026) a ajouté un typage amélioré des tâches et une meilleure intégration Django via django-celery-results pour stocker les résultats des tâches en base de données.
Comparaison des Caractéristiques de Performance
Les signaux ajoutent de la latence à la requête. Chaque récepteur connecté s'exécute avant que la réponse ne soit renvoyée. Avec trois récepteurs moyennant 50ms chacun, la requête prend 150ms de plus.
Celery ajoute une latence minimale (typiquement 1-5ms pour la mise en file), mais introduit une cohérence éventuelle. L'email est envoyé "éventuellement", pas avant la réponse.
| Facteur | Signaux Django | Tâches Celery |
|---|---|---|
| Exécution | Synchrone, même processus | Asynchrone, processus worker |
| Impact latence | Ajoute au temps de requête | ~1-5ms overhead mise en file |
| Gestion des échecs | Casse la requête | Réessaie indépendamment |
| Portée transaction | Dans la transaction | Hors transaction |
| Appels services externes | Bloque la réponse | S'exécute en arrière-plan |
| Complexité | Configuration minimale | Nécessite broker + workers |
Quand les Signaux Sont le Bon Choix
Les signaux conviennent aux scénarios où l'effet secondaire doit se terminer avant de continuer et où un échec doit annuler l'opération.
L'invalidation de cache fonctionne bien avec les signaux. Quand un Product est mis à jour, sa représentation en cache doit être invalidée immédiatement. Un cache obsolète même pendant quelques secondes provoque l'affichage de prix ou de stocks incorrects.
# signals.py
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
from django.core.cache import cache
from .models import Product
@receiver([post_save, post_delete], sender=Product)
def clear_product_cache(sender, instance, **kwargs):
cache.delete(f"product:{instance.id}")
cache.delete(f"product:{instance.slug}")
# Vide tous les caches de liste incluant ce produit
cache.delete_pattern(f"products:category:{instance.category_id}:*")La journalisation d'audit où le log doit exister avant le retour de la réponse convient également aux signaux. Si un administrateur supprime un utilisateur, le log d'audit doit enregistrer cette suppression de manière atomique avec l'opération de suppression.
La dénormalisation de champs entre modèles liés bénéficie des signaux. Quand le statut d'une Order passe à "expédié", la mise à jour d'un champ dénormalisé last_shipped_at sur le Customer se fait immédiatement.
Prêt à réussir tes entretiens Django ?
Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.
Quand les Tâches Celery Sont le Bon Choix
Celery convient aux scénarios impliquant des services externes, des calculs longs, ou des opérations qui peuvent échouer et être réessayées indépendamment.
L'envoi d'emails ne devrait jamais bloquer les requêtes. Les serveurs SMTP ont une latence imprévisible, font parfois timeout, et limitent le débit des expéditeurs. Une tâche Celery réessaie les emails échoués sans affecter l'expérience utilisateur.
La génération de PDF pour les factures ou rapports prend plusieurs secondes. Générer de manière synchrone donne l'impression que l'interface est cassée. Mettre en file la tâche, retourner un statut "en traitement", et laisser l'utilisateur télécharger quand c'est prêt.
Les appels d'API tierces appartiennent à Celery. Appeler Stripe, Twilio, ou tout service externe de manière synchrone couple le temps de réponse au leur. Quand leur API ralentit, l'application ralentit.
# tasks.py
from celery import shared_task
import stripe
from .models import Subscription
@shared_task(bind=True, max_retries=5, retry_backoff=True)
def sync_stripe_subscription(self, subscription_id: int):
"""Synchronise l'abonnement local avec l'état Stripe."""
try:
sub = Subscription.objects.get(id=subscription_id)
stripe_sub = stripe.Subscription.retrieve(sub.stripe_id)
sub.status = stripe_sub.status
sub.current_period_end = stripe_sub.current_period_end
sub.save(update_fields=['status', 'current_period_end'])
except stripe.error.RateLimitError as exc:
# Stripe nous a limité, réessayer avec backoff exponentiel
raise self.retry(exc=exc)
except Subscription.DoesNotExist:
return # Abonnement supprimé localement, rien à synchroniserLe paramètre retry_backoff=True dans Celery 5.x active le backoff exponentiel : premier réessai après 1 seconde, puis 2, puis 4, jusqu'au maximum. Cela évite de marteler une API limitée en débit.
Le Patron Hybride : Signaux Déclenchant des Tâches
L'architecture la plus robuste utilise les signaux pour la coordination immédiate et légère, et Celery pour le travail lourd réel.
# signals.py
from django.db import transaction
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Order
from .tasks import (
send_order_confirmation,
notify_warehouse,
update_analytics,
)
@receiver(post_save, sender=Order)
def on_order_created(sender, instance, created, **kwargs):
if not created:
return # Ne traite que les nouvelles commandes
# Met en file tout le travail async après le commit de la transaction
transaction.on_commit(lambda: (
send_order_confirmation.delay(instance.id),
notify_warehouse.delay(instance.id),
update_analytics.delay('order_created', instance.id),
))Ce patron garantit :
- La commande existe en base de données avant l'exécution des tâches (transaction validée)
- Aucun travail async ne se produit si la transaction fait un rollback
- La réponse HTTP est retournée immédiatement
- Chaque tâche peut échouer et être réessayée indépendamment
Questions d'Entretien sur Django Signals vs Celery
Les entretiens techniques pour les postes Django explorent fréquemment cette distinction. Voici des questions qui différencient les candidats expérimentés de ceux qui ont mémorisé la documentation.
Q : Un gestionnaire de signal envoie un email. La transaction de base de données fait un rollback. Que se passe-t-il ?
L'email est envoyé quand même. Les gestionnaires de signaux s'exécutent pendant la transaction, pas après le commit. L'email part, mais le changement en base de données qui l'a déclenché ne persiste jamais. Pour corriger cela, envelopper l'appel email dans transaction.on_commit(), ou mieux, mettre en file une tâche Celery dans on_commit().
Q : Comment empêcher un signal de se déclencher lors d'opérations en masse ?
Les méthodes bulk_create(), bulk_update() et QuerySet.update() de Django ne déclenchent pas les signaux. C'est intentionnel pour la performance. Si le comportement du signal est nécessaire, itérer et sauvegarder individuellement (en acceptant le coût de performance) ou dispatcher manuellement le signal après l'opération en masse.
Q : Une tâche Celery référence request.user. Pourquoi échoue-t-elle ?
Les tâches Celery s'exécutent dans un processus séparé sans accès au contexte de la requête HTTP. Passer l'ID utilisateur comme argument de tâche et récupérer l'objet User à l'intérieur de la tâche. Ne jamais passer d'instances de modèles Django directement comme arguments de tâche, car elles peuvent devenir obsolètes ou échouer à la sérialisation.
Q : Comment tester les gestionnaires de signaux de manière isolée ?
Déconnecter le signal, appeler la fonction du gestionnaire directement avec des arguments mockés, puis reconnecter. Les méthodes Signal.disconnect() et Signal.connect() de Django le permettent. Le décorateur @factory.django.mute_signals de la bibliothèque factory_boy aide également en silençant temporairement les signaux pendant la configuration des tests.
# tests.py
from django.test import TestCase
from django.db.models.signals import post_save
from unittest.mock import patch, MagicMock
from .models import Product
from .signals import invalidate_product_cache
class SignalTests(TestCase):
def test_cache_invalidation_called(self):
# Déconnecter pour empêcher le déclenchement automatique
post_save.disconnect(invalidate_product_cache, sender=Product)
try:
product = Product.objects.create(name="Test", price=100)
with patch('myapp.signals.cache') as mock_cache:
# Appeler le gestionnaire directement
invalidate_product_cache(
sender=Product,
instance=product,
created=True
)
mock_cache.delete.assert_called()
finally:
# Reconnecter pour les autres tests
post_save.connect(invalidate_product_cache, sender=Product)Anti-Patrons Courants à Éviter
Les chaînes de signaux circulaires se produisent quand le Signal A déclenche une sauvegarde sur le Modèle B, dont le signal déclenche une sauvegarde sur le Modèle A. La récursion continue jusqu'au débordement de pile ou jusqu'à ce qu'une garde de récursion l'arrête. Concevoir les signaux pour être terminaux : ils observent et réagissent, mais ne déclenchent pas d'autres changements observables.
Les calculs lourds dans les signaux bloquent chaque requête qui déclenche le signal. Un signal qui redimensionne des images, génère des miniatures, ou appelle des APIs externes devrait plutôt mettre en file une tâche Celery.
Les échecs silencieux de signaux cachent les bugs. Par défaut, Django attrape les exceptions dans les gestionnaires de signaux et les journalise, permettant à la requête de continuer. Cela masque les erreurs. Considérer si un gestionnaire en échec doit annuler l'opération ou être vraiment best-effort.
# settings.py
# Faire propager les exceptions des gestionnaires de signaux
DEBUG = True # En dev, les exceptions se propagent naturellement
# En production, utiliser Sentry ou similaire pour capturer les erreurs
import sentry_sdk
sentry_sdk.init(dsn="...")Passe à la pratique !
Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.
Cadre de Décision pour la Gestion d'Événements Django
Le choix entre signaux et Celery devient simple avec une approche systématique :
- Utiliser les signaux quand : l'opération doit se terminer avant la réponse, prend moins de 50ms, implique uniquement des opérations locales de base de données ou de cache, et l'échec doit annuler l'opération principale
- Utiliser Celery quand : l'opération peut se produire éventuellement, implique des services externes, prend plus de 100ms, bénéficie d'une logique de réessai, ou ne doit pas bloquer l'utilisateur
- Utiliser les signaux déclenchant Celery quand : un accusé de réception immédiat est nécessaire mais le travail réel est async, ou quand plusieurs tâches async doivent se déclencher depuis un événement de base de données
- Éviter les signaux entièrement quand : la logique est assez complexe pour justifier des appels de service explicites, quand le débogage des chaînes de signaux devient difficile, ou quand le même effet peut être obtenu avec un simple appel de méthode
La fonctionnalité de tâches en arrière-plan de Django 6.0 offre un terrain d'entente : un support de tâches async intégré sans l'overhead opérationnel de Celery. Pour les nouveaux projets démarrant fin 2026, évaluer si les tâches en arrière-plan natives de Django répondent aux exigences avant d'ajouter Celery.
Tu saurais repérer le bug en Django ?
Un vrai bout de code, un bug caché, une tentative par jour. Sans compte pour essayer.

Écrit par
Anthony Fillion-MailletFondateur de SharpSkill
Développeur fullstack depuis plus de 10 ans. Il dirige SharpSkill et répond de tout ce qui y est publié.
Mis à jour le 2 septembre 2026
Tags
Partager
Articles similaires

Django et Celery : traitement asynchrone des tâches et questions d'entretien 2026
Guide Django Celery avec exemples pratiques, routage de tâches, planification Beat, configuration production et questions d'entretien technique 2026.

Vues asynchrones Django et ASGI en 2026 : performance et questions d’entretien
Une plongée en profondeur dans les vues asynchrones Django et ASGI en 2026 : leur fonctionnement interne, quel serveur déployer, l’ORM asynchrone et le piège SynchronousOnlyOperation, plus des questions d’entretien.

Django 5.2 : Middleware Personnalisé et Gestion des Signaux pour les Entretiens Techniques
Guide complet sur les middleware personnalisés et les signaux dans Django 5.2. Exemples pratiques de middleware de logging, middleware asynchrone, signaux pre_save/post_save et questions fréquentes en entretien.