Tre strumenti, un solo problema
Quando in un'applicazione Django qualcosa deve succedere automaticamente — un'email da mandare, un record da aggiornare, un file da generare — la domanda è quasi sempre la stessa: lo faccio con un signal, un management command, o un task Celery?
La risposta sbagliata è scegliere uno dei tre e usarlo per tutto. La risposta giusta è capire che risolvono problemi diversi e si combinano naturalmente — spesso nello stesso flusso.

Signals: reagire agli eventi del modello
I signals di Django sono il meccanismo di comunicazione tra componenti. Permettono a una parte del codice di notificare altre parti che qualcosa è successo, senza che ci sia un accoppiamento diretto tra i due.
Il caso più comune è post_save — viene emesso ogni volta che un oggetto viene salvato nel database:
from django.db.models.signals import post_save
from django.dispatch import receiver
from myapp.models import Progetto
@receiver(post_save, sender=Progetto)
def progetto_creato(sender, instance, created, **kwargs):
if created:
print(f"Nuovo progetto creato: {instance.titolo}")Il decorator @receiver collega la funzione al signal. Ogni volta che un Progetto viene salvato per la prima volta (created=True), la funzione viene eseguita.
I signals disponibili più utili sono:
- pre_save / post_save — prima e dopo il salvataggio
- pre_delete / post_delete — prima e dopo l'eliminazione
- m2m_changed — quando cambia una relazione ManyToMany
- request_started / request_finished — inizio e fine di ogni request HTTP
Quando usare i signals
I signals hanno senso quando vuoi reagire a un evento del modello in modo trasversale — cioè senza modificare il codice del modello stesso o della view. Sono utili per:
- Aggiornare dati derivati quando un modello cambia
- Invalidare cache quando un oggetto viene modificato
- Logging e auditing automatico
- Notifiche leggere che non richiedono operazioni pesanti
Quando NON usare i signals
I signals hanno un costo nascosto: rendono il flusso del codice difficile da seguire. Se guardi una view e vedi progetto.save(), non sai quanti signal vengono scatenati e cosa fanno. Per operazioni critiche o complesse, è spesso più chiaro chiamare esplicitamente una funzione nel punto giusto del codice.
Un altro limite fondamentale: i signals vengono eseguiti sincronicamente, nel thread della request. Se il tuo signal fa qualcosa di lento — chiamate HTTP, generazione PDF, invio email — stai bloccando la response. Qui entrano in gioco i task Celery.
Management commands: operazioni batch e schedulabili
I management commands sono script Python che girano fuori dal ciclo request/response di Django, invocabili via python manage.py nome_comando. Django ne include molti (migrate, collectstatic, createsuperuser) e puoi crearne di custom.
La struttura base è semplice:
from django.core.management.base import BaseCommand
from myapp.models import Progetto
from django.utils import timezone
class Command(BaseCommand):
help = "Chiude i progetti scaduti"
def add_arguments(self, parser):
parser.add_argument("--dry-run", action="store_true")
def handle(self, *args, **options):
scaduti = Progetto.objects.filter(
data_scadenza__lt=timezone.now(),
stato="attivo"
)
count = scaduti.count()
if options["dry_run"]:
self.stdout.write(f"Trovati {count} progetti da chiudere (dry run)")
return
scaduti.update(stato="scaduto")
self.stdout.write(
self.style.SUCCESS(f"Chiusi {count} progetti scaduti")
)Il file va in myapp/management/commands/chiudi_progetti_scaduti.py. La struttura delle directory è rigida — Django la cerca lì.
Quando usare i management commands
I management commands sono lo strumento giusto per:
- Operazioni batch che agiscono su molti record — migrazione dati, pulizia, aggiornamenti massivi
- Script di manutenzione che vuoi eseguire manualmente o schedulare con cron
- Import/export dati
- Seed del database in sviluppo
- Operazioni one-shot dopo un deploy
Il vantaggio rispetto a uno script Python standalone è l'accesso diretto a tutto l'ambiente Django — ORM, settings, logging — senza configurazione aggiuntiva.
Il limite è che un management command non è distribuibile e non scala orizzontalmente — gira su una sola macchina, in modo sincrono. Per operazioni che richiedono parallelismo o distribuzione, servono i task Celery.
Celery: task asincroni e distribuiti
Celery è un sistema di code di task. Invece di eseguire un'operazione immediatamente nel thread corrente, la metti in coda e un worker separato la esegue quando può. Il vantaggio principale è che la response HTTP non aspetta — l'utente ottiene la risposta subito, il lavoro pesante avviene in background.
La configurazione base con Redis come broker:
CELERY_BROKER_URL = "redis://localhost:6379/0"
CELERY_RESULT_BACKEND = "redis://localhost:6379/0"Un task semplice:
from celery import shared_task
from myapp.models import Progetto
@shared_task
def genera_report_progetto(progetto_id: int):
progetto = Progetto.objects.get(id=progetto_id)
pdf = genera_pdf(progetto)
progetto.report = pdf
progetto.save()
return f"Report generato per {progetto.titolo}"Per eseguirlo in background dalla view:
genera_report_progetto.delay(progetto.id).delay() mette il task in coda e ritorna immediatamente. Il worker Celery lo eseguirà appena disponibile.
Quando usare Celery
Celery è lo strumento giusto per:
- Operazioni lente che non devono bloccare la response — invio email, generazione PDF, chiamate API esterne
- Operazioni che possono fallire e richiedono retry automatici
- Task che devono girare su più worker in parallelo
- Operazioni schedulate con Celery Beat (cronjob in Python)
Il costo di Celery è la complessità infrastrutturale — richiede un broker (Redis o RabbitMQ), worker separati, e un sistema di monitoraggio. Non è lo strumento giusto per operazioni semplici che si risolvono in pochi millisecondi.
Come si combinano: il flusso completo
La potenza vera emerge quando i tre strumenti lavorano insieme. Un esempio concreto da Skara: quando una commissione di selezione viene finalizzata, bisogna generare i verbali PDF, inviare le notifiche ai candidati, e aggiornare le statistiche del progetto.
Il signal reagisce all'evento:
from django.db.models.signals import post_save
from django.dispatch import receiver
from myapp.models import Commissione
from myapp.tasks import processa_commissione_finalizzata
@receiver(post_save, sender=Commissione)
def commissione_finalizzata(sender, instance, **kwargs):
if instance.stato == "finalizzata":
processa_commissione_finalizzata.delay(instance.id)Il task Celery fa il lavoro pesante in background:
from celery import shared_task
from myapp.models import Commissione
@shared_task(bind=True, max_retries=3)
def processa_commissione_finalizzata(self, commissione_id: int):
try:
commissione = Commissione.objects.select_related(
"progetto"
).prefetch_related("candidati").get(id=commissione_id)
genera_verbali_pdf(commissione)
invia_notifiche_candidati(commissione)
aggiorna_statistiche_progetto(commissione.progetto)
except Exception as exc:
raise self.retry(exc=exc, countdown=60)Il management command per la manutenzione:
class Command(BaseCommand):
help = "Processa le commissioni finalizzate non elaborate"
def handle(self, *args, **options):
commissioni = Commissione.objects.filter(
stato="finalizzata",
elaborata=False
)
for c in commissioni:
processa_commissione_finalizzata.delay(c.id)
self.stdout.write(f"Schedulata elaborazione commissione {c.id}")Il management command è il safety net — se per qualche motivo un task è andato perso o un worker era giù, questo comando recupera i record non elaborati e li rimette in coda. Si può schedulare con cron ogni ora come failsafe.
La regola pratica
Scegli il tool in base a quando e dove deve succedere l'automazione:
- Signal — reagisci a un evento del modello, sincrono, leggero
- Management command — operazione batch, schedulabile, manuale o via cron
- Celery task — operazione lenta, asincrona, con retry, distribuibile
E ricorda: i tre si combinano. Il pattern signal → Celery task è uno dei più usati in Django proprio perché sfrutta i punti di forza di entrambi — il signal intercetta l'evento senza modificare il modello, Celery gestisce il lavoro pesante senza bloccare la request.