Django Signals vs Celery Tasks en 2026: Cuándo Usar Cada Uno y Preguntas de Entrevista
Aprende cuándo usar señales Django versus tareas Celery. Cubre el manejo de eventos síncrono vs asíncrono, implicaciones de rendimiento y preguntas de entrevista para desarrolladores Django.

Las señales Django y las tareas Celery permiten manejar eventos en aplicaciones Django, pero resuelven problemas diferentes. Las señales se ejecutan de manera síncrona dentro del ciclo petición-respuesta, mientras que Celery delega el trabajo a workers en segundo plano. Elegir la herramienta equivocada lleva a respuestas lentas, condiciones de carrera o arquitecturas innecesariamente complejas.
Usar señales Django para efectos secundarios ligeros y síncronos que deben completarse antes de la respuesta. Usar Celery para cualquier cosa que tome más de 100ms, involucre servicios externos, o pueda fallar independientemente de la petición principal.
Cómo Funcionan las Señales Django Internamente
Las señales Django implementan el patrón observador. Cuando un modelo guarda, elimina, o cuando una petición inicia o termina, Django despacha una señal. Cualquier función conectada a esa señal se ejecuta inmediatamente, en la misma transacción de base de datos y el mismo hilo.
# 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):
# Se ejecuta síncronamente después de Product.save()
cache_key = f"product:{instance.id}"
cache.delete(cache_key)
# También invalida el listado de categoría
cache.delete(f"category:{instance.category_id}:products")El receptor de señal se ejecuta dentro de la misma transacción de base de datos. Si la transacción hace rollback, los efectos del manejador de señal permanecen. Esto importa para la invalidación de caché: el caché se limpia, pero el cambio en base de datos nunca persiste. El resultado es un cache miss que recarga datos obsoletos.
Django 5.2 introdujo transaction.on_commit() para resolver esto. Envolver la lógica de la señal en on_commit asegura la ejecución solo después de un commit exitoso:
# 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):
# Diferir hasta que la transacción se confirme
transaction.on_commit(
lambda: reindex_product.delay(instance.id)
)Este patrón conecta señales y Celery: la señal se dispara de manera síncrona, pero el trabajo real ocurre de manera asíncrona después de confirmar la transacción.
Modelo de Ejecución de Tareas Celery
Celery ejecuta tareas en procesos worker separados. Una vista Django encola una tarea serializando sus argumentos a un broker de mensajes (Redis o RabbitMQ). Un worker toma el mensaje y ejecuta la tarea independientemente de la petición HTTP original.
# 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):
"""Envía email de confirmación después de realizar el pedido."""
try:
order = Order.objects.select_related('user').get(id=order_id)
send_mail(
subject=f"Pedido #{order.id} Confirmado",
message=f"Tu pedido por {order.total} ha sido realizado.",
from_email="pedidos@example.com",
recipient_list=[order.user.email],
)
except Order.DoesNotExist:
# El pedido fue eliminado antes de ejecutar la tarea
return
except Exception as exc:
# Reintentar en fallos transitorios (timeout SMTP, etc.)
raise self.retry(exc=exc)La tarea se ejecuta en un proceso diferente, potencialmente en una máquina diferente. No tiene acceso al contexto de la petición original. Si el pedido se elimina entre el encolado y la ejecución, la tarea debe manejar eso graciosamente.
Celery 5.4 (versión estable actual en septiembre 2026) agregó tipado mejorado de tareas y mejor integración con Django a través de django-celery-results para almacenar resultados de tareas en la base de datos.
Comparación de Características de Rendimiento
Las señales agregan latencia a la petición. Cada receptor conectado se ejecuta antes de retornar la respuesta. Con tres receptores promediando 50ms cada uno, la petición toma 150ms adicionales.
Celery agrega latencia mínima (típicamente 1-5ms para encolar), pero introduce consistencia eventual. El email se envía "eventualmente", no antes de la respuesta.
| Factor | Señales Django | Tareas Celery |
|---|---|---|
| Ejecución | Síncrona, mismo proceso | Asíncrona, proceso worker |
| Impacto en latencia | Se suma al tiempo de petición | ~1-5ms overhead de encolado |
| Manejo de fallos | Rompe la petición | Reintenta independientemente |
| Alcance de transacción | Dentro de la transacción | Fuera de la transacción |
| Llamadas a servicios externos | Bloquea la respuesta | Se ejecuta en segundo plano |
| Complejidad | Configuración mínima | Requiere broker + workers |
Cuándo las Señales Son la Elección Correcta
Las señales encajan en escenarios donde el efecto secundario debe completarse antes de continuar y donde un fallo debe abortar la operación.
La invalidación de caché funciona bien con señales. Cuando un Product se actualiza, su representación en caché debe invalidarse inmediatamente. Un caché obsoleto incluso por unos segundos causa que se muestren precios o inventarios incorrectos.
# 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}")
# Limpia cualquier caché de lista que pueda incluir este producto
cache.delete_pattern(f"products:category:{instance.category_id}:*")El registro de auditoría donde el log debe existir antes de retornar la respuesta también encaja con señales. Si un administrador elimina un usuario, el log de auditoría debe registrar esa eliminación atómicamente con la operación de eliminación.
La desnormalización de campos entre modelos relacionados se beneficia de las señales. Cuando el estado de una Order cambia a "enviado", actualizar un campo desnormalizado last_shipped_at en el Customer sucede inmediatamente.
¿Listo para aprobar tus entrevistas de Django?
Practica con nuestros simuladores interactivos, flashcards y tests técnicos.
Cuándo las Tareas Celery Son la Elección Correcta
Celery encaja en escenarios que involucran servicios externos, cálculos de larga duración, u operaciones que pueden fallar y reintentarse independientemente.
El envío de emails nunca debería bloquear peticiones. Los servidores SMTP tienen latencia impredecible, ocasionalmente hacen timeout, y limitan el rate de los remitentes. Una tarea Celery reintenta emails fallidos sin afectar la experiencia del usuario.
La generación de PDFs para facturas o reportes toma segundos. Generar síncronamente hace que la UI se sienta rota. Encolar la tarea, retornar un estado "procesando", y dejar que el usuario descargue cuando esté listo.
Las llamadas a APIs de terceros pertenecen a Celery. Llamar a Stripe, Twilio, o cualquier servicio externo síncronamente acopla el tiempo de respuesta al de ellos. Cuando su API se ralentiza, la aplicación se ralentiza.
# 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):
"""Sincroniza la suscripción local con el estado de 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 nos limitó, reintentar con backoff exponencial
raise self.retry(exc=exc)
except Subscription.DoesNotExist:
return # Suscripción eliminada localmente, nada que sincronizarEl parámetro retry_backoff=True en Celery 5.x habilita backoff exponencial: primer reintento después de 1 segundo, luego 2, luego 4, hasta el máximo. Esto previene bombardear una API con rate limit.
El Patrón Híbrido: Señales Disparando Tareas
La arquitectura más robusta usa señales para coordinación inmediata y ligera, y Celery para el trabajo pesado real.
# 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 # Solo maneja pedidos nuevos
# Encola todo el trabajo async después del commit de la transacción
transaction.on_commit(lambda: (
send_order_confirmation.delay(instance.id),
notify_warehouse.delay(instance.id),
update_analytics.delay('order_created', instance.id),
))Este patrón asegura:
- El pedido existe en la base de datos antes de que las tareas se ejecuten (transacción confirmada)
- Ningún trabajo async ocurre si la transacción hace rollback
- La respuesta HTTP retorna inmediatamente
- Cada tarea puede fallar y reintentarse independientemente
Preguntas de Entrevista sobre Django Signals vs Celery
Las entrevistas técnicas para posiciones Django frecuentemente exploran esta distinción. Aquí hay preguntas que separan a los candidatos experimentados de aquellos que memorizaron la documentación.
P: Un manejador de señal envía un email. La transacción de base de datos hace rollback. ¿Qué sucede?
El email se envía de todos modos. Los manejadores de señales se ejecutan durante la transacción, no después del commit. El email sale, pero el cambio en base de datos que lo disparó nunca persiste. Para arreglar esto, envolver la llamada de email en transaction.on_commit(), o mejor, encolar una tarea Celery dentro de on_commit().
P: ¿Cómo se previene que una señal se dispare durante operaciones masivas?
Los métodos bulk_create(), bulk_update() y QuerySet.update() de Django no disparan señales. Esto es intencional por rendimiento. Si el comportamiento de la señal es necesario, iterar y guardar individualmente (aceptando el costo de rendimiento) o despachar manualmente la señal después de la operación masiva.
P: Una tarea Celery referencia request.user. ¿Por qué falla?
Las tareas Celery se ejecutan en un proceso separado sin acceso al contexto de la petición HTTP. Pasar el ID del usuario como argumento de la tarea y obtener el objeto User dentro de la tarea. Nunca pasar instancias de modelos Django directamente como argumentos de tareas, ya que pueden volverse obsoletas o fallar al serializar.
P: ¿Cómo se prueban los manejadores de señales de forma aislada?
Desconectar la señal, llamar la función del manejador directamente con argumentos mock, luego reconectar. Los métodos Signal.disconnect() y Signal.connect() de Django permiten esto. El decorador @factory.django.mute_signals de la biblioteca factory_boy también ayuda silenciando temporalmente señales durante la configuración de 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):
# Desconectar para prevenir disparo automático
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:
# Llamar al manejador directamente
invalidate_product_cache(
sender=Product,
instance=product,
created=True
)
mock_cache.delete.assert_called()
finally:
# Reconectar para otros tests
post_save.connect(invalidate_product_cache, sender=Product)Antipatrones Comunes a Evitar
Cadenas de señales circulares ocurren cuando la Señal A dispara un guardado en el Modelo B, cuya señal dispara un guardado en el Modelo A. La recursión continúa hasta que el stack desborda o una guarda de recursión la detiene. Diseñar las señales para ser terminales: observan y reaccionan, pero no disparan más cambios observables.
Cálculos pesados en señales bloquean cada petición que dispara la señal. Una señal que redimensiona imágenes, genera miniaturas, o llama APIs externas debería en cambio encolar una tarea Celery.
Fallos silenciosos de señales ocultan bugs. Por defecto, Django captura excepciones en manejadores de señales y las registra, permitiendo que la petición continúe. Esto enmascara errores. Considerar si un manejador fallido debería abortar la operación o verdaderamente ser best-effort.
# settings.py
# Hacer que las excepciones de manejadores de señales se propaguen
DEBUG = True # En dev, las excepciones se propagan naturalmente
# En producción, usar Sentry o similar para capturar errores de señales
import sentry_sdk
sentry_sdk.init(dsn="...")¡Empieza a practicar!
Pon a prueba tu conocimiento con nuestros simuladores de entrevista y tests técnicos.
Marco de Decisión para Manejo de Eventos Django
Seleccionar entre señales y Celery se vuelve simple con un enfoque sistemático:
- Usar señales cuando: la operación debe completarse antes de la respuesta, toma menos de 50ms, involucra solo operaciones locales de base de datos o caché, y el fallo debería abortar la operación principal
- Usar Celery cuando: la operación puede suceder eventualmente, involucra servicios externos, toma más de 100ms, se beneficia de lógica de reintento, o no debería bloquear al usuario
- Usar señales disparando Celery cuando: se necesita reconocimiento inmediato pero el trabajo real es async, o cuando múltiples tareas async deberían dispararse desde un evento de base de datos
- Evitar señales completamente cuando: la lógica es lo suficientemente compleja para justificar llamadas de servicio explícitas, cuando depurar cadenas de señales se vuelve difícil, o cuando el mismo efecto puede lograrse con una simple llamada de método
La característica de tareas en segundo plano de Django 6.0 proporciona un punto medio: soporte de tareas async integrado sin el overhead operacional de Celery. Para proyectos nuevos comenzando a finales de 2026, evaluar si las tareas en segundo plano nativas de Django cumplen los requisitos antes de agregar Celery.
¿Sabrías detectar el bug en Django?
Un fragmento real, un bug oculto, un intento al día. Sin cuenta para probar.

Escrito por
Anthony Fillion-MailletFundador de SharpSkill
Desarrollador fullstack desde hace más de 10 años. Dirige SharpSkill y responde por todo lo que se publica aquí.
Actualizado el 2 de septiembre de 2026
Etiquetas
Compartir
Artículos relacionados

Django y Celery en 2026: Procesamiento Asíncrono y Preguntas de Entrevista
Guía completa de Django y Celery en 2026: configuración, colas, tareas periódicas, monitoreo con Flower, comparativa con Django 6.0 y preguntas de entrevista técnica.

Vistas asíncronas de Django y ASGI en 2026: rendimiento y preguntas de entrevista
Un análisis a fondo de las vistas asíncronas de Django y ASGI en 2026: cómo funcionan por dentro, qué servidor desplegar, el ORM asíncrono y la trampa de SynchronousOnlyOperation, además de preguntas de entrevista.

Django 5.2: Middleware Personalizado y Manejo de Señales para Entrevistas Técnicas
Guía completa sobre middleware personalizado y señales en Django 5.2. Implementación de middleware de logging, middleware asíncrono, señales pre_save/post_save y preguntas frecuentes de entrevista.