Django Signals vs Celery Tasks em 2026: Quando Usar Cada Um e Perguntas de Entrevista
Aprenda quando usar signals Django versus tasks Celery. Aborda o tratamento de eventos síncrono vs assíncrono, implicações de performance e perguntas de entrevista para desenvolvedores Django.

Os signals Django e as tasks Celery permitem lidar com eventos em aplicações Django, mas resolvem problemas diferentes. Os signals executam de forma síncrona dentro do ciclo requisição-resposta, enquanto o Celery delega o trabalho para workers em background. Escolher a ferramenta errada leva a respostas lentas, condições de corrida ou arquiteturas desnecessariamente complexas.
Usar signals Django para efeitos colaterais leves e síncronos que devem ser concluídos antes da resposta. Usar Celery para qualquer coisa que leve mais de 100ms, envolva serviços externos, ou possa falhar independentemente da requisição principal.
Como os Signals Django Funcionam Internamente
Os signals Django implementam o padrão observer. Quando um modelo salva, deleta, ou quando uma requisição inicia ou termina, o Django dispara um signal. Qualquer função conectada a esse signal executa imediatamente, na mesma transação de banco de dados e na mesma 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):
# Executa de forma síncrona após Product.save()
cache_key = f"product:{instance.id}"
cache.delete(cache_key)
# Também invalida a listagem da categoria
cache.delete(f"category:{instance.category_id}:products")O receptor do signal executa dentro da mesma transação de banco de dados. Se a transação fizer rollback, os efeitos do handler do signal permanecem. Isso importa para invalidação de cache: o cache é limpo, mas a mudança no banco de dados nunca persiste. O resultado é um cache miss que recarrega dados desatualizados.
O Django 5.2 introduziu transaction.on_commit() para resolver isso. Envolver a lógica do signal em on_commit garante a execução apenas após um commit bem-sucedido:
# 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):
# Adiar até que a transação seja confirmada
transaction.on_commit(
lambda: reindex_product.delay(instance.id)
)Esse padrão conecta signals e Celery: o signal dispara de forma síncrona, mas o trabalho real acontece de forma assíncrona após a confirmação da transação.
Modelo de Execução de Tasks Celery
O Celery executa tasks em processos worker separados. Uma view Django enfileira uma task serializando seus argumentos para um broker de mensagens (Redis ou RabbitMQ). Um worker pega a mensagem e executa a task independentemente da requisição 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):
"""Envia email de confirmação após realizar o pedido."""
try:
order = Order.objects.select_related('user').get(id=order_id)
send_mail(
subject=f"Pedido #{order.id} Confirmado",
message=f"Seu pedido de {order.total} foi realizado.",
from_email="pedidos@example.com",
recipient_list=[order.user.email],
)
except Order.DoesNotExist:
# O pedido foi deletado antes da task executar
return
except Exception as exc:
# Tentar novamente em falhas transitórias (timeout SMTP, etc.)
raise self.retry(exc=exc)A task executa em um processo diferente, potencialmente em uma máquina diferente. Ela não tem acesso ao contexto da requisição original. Se o pedido for deletado entre o enfileiramento e a execução, a task deve tratar isso de forma adequada.
O Celery 5.4 (versão estável atual em setembro de 2026) adicionou tipagem aprimorada de tasks e melhor integração com Django através do django-celery-results para armazenar resultados de tasks no banco de dados.
Comparação de Características de Performance
Os signals adicionam latência à requisição. Cada receptor conectado executa antes da resposta ser retornada. Com três receptores com média de 50ms cada, a requisição leva 150ms a mais.
O Celery adiciona latência mínima (tipicamente 1-5ms para enfileirar), mas introduz consistência eventual. O email é enviado "eventualmente", não antes da resposta.
| Fator | Signals Django | Tasks Celery |
|---|---|---|
| Execução | Síncrona, mesmo processo | Assíncrona, processo worker |
| Impacto na latência | Adiciona ao tempo de requisição | ~1-5ms overhead de enfileiramento |
| Tratamento de falhas | Quebra a requisição | Tenta novamente independentemente |
| Escopo da transação | Dentro da transação | Fora da transação |
| Chamadas a serviços externos | Bloqueia a resposta | Executa em background |
| Complexidade | Configuração mínima | Requer broker + workers |
Quando os Signals São a Escolha Certa
Os signals se encaixam em cenários onde o efeito colateral deve ser concluído antes de continuar e onde uma falha deve abortar a operação.
A invalidação de cache funciona bem com signals. Quando um Product é atualizado, sua representação em cache deve ser invalidada imediatamente. Um cache desatualizado mesmo por alguns segundos faz com que preços ou estoques incorretos sejam exibidos.
# 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}")
# Limpa quaisquer caches de lista que possam incluir este produto
cache.delete_pattern(f"products:category:{instance.category_id}:*")O registro de auditoria onde o log deve existir antes da resposta ser retornada também se encaixa com signals. Se um administrador deleta um usuário, o log de auditoria deve registrar essa deleção atomicamente com a operação de deleção.
A desnormalização de campos entre modelos relacionados se beneficia dos signals. Quando o status de uma Order muda para "enviado", atualizar um campo desnormalizado last_shipped_at no Customer acontece imediatamente.
Pronto para mandar bem nas entrevistas de Django?
Pratique com nossos simuladores interativos, flashcards e testes tecnicos.
Quando as Tasks Celery São a Escolha Certa
O Celery se encaixa em cenários envolvendo serviços externos, computações de longa duração, ou operações que podem falhar e ser reexecutadas independentemente.
O envio de emails nunca deve bloquear requisições. Servidores SMTP têm latência imprevisível, ocasionalmente dão timeout, e limitam a taxa de remetentes. Uma task Celery tenta novamente emails com falha sem afetar a experiência do usuário.
A geração de PDFs para faturas ou relatórios leva segundos. Gerar de forma síncrona faz a UI parecer quebrada. Enfileirar a task, retornar um status "processando", e deixar o usuário baixar quando estiver pronto.
As chamadas a APIs de terceiros pertencem ao Celery. Chamar Stripe, Twilio, ou qualquer serviço externo de forma síncrona acopla o tempo de resposta ao deles. Quando a API deles fica lenta, a aplicação fica lenta.
# 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 a assinatura local com o estado do 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 limitou, tentar novamente com backoff exponencial
raise self.retry(exc=exc)
except Subscription.DoesNotExist:
return # Assinatura deletada localmente, nada para sincronizarO parâmetro retry_backoff=True no Celery 5.x habilita backoff exponencial: primeira tentativa após 1 segundo, depois 2, depois 4, até o máximo. Isso evita bombardear uma API com rate limit.
O Padrão Híbrido: Signals Disparando Tasks
A arquitetura mais robusta usa signals para coordenação imediata e leve, e Celery para o trabalho 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 # Só trata pedidos novos
# Enfileira todo o trabalho async após o commit da transação
transaction.on_commit(lambda: (
send_order_confirmation.delay(instance.id),
notify_warehouse.delay(instance.id),
update_analytics.delay('order_created', instance.id),
))Esse padrão garante:
- O pedido existe no banco de dados antes das tasks executarem (transação confirmada)
- Nenhum trabalho async acontece se a transação fizer rollback
- A resposta HTTP retorna imediatamente
- Cada task pode falhar e ser reexecutada independentemente
Perguntas de Entrevista sobre Django Signals vs Celery
Entrevistas técnicas para posições Django frequentemente exploram essa distinção. Aqui estão perguntas que separam candidatos experientes daqueles que memorizaram a documentação.
P: Um handler de signal envia um email. A transação de banco de dados faz rollback. O que acontece?
O email é enviado de qualquer forma. Os handlers de signal executam durante a transação, não após o commit. O email sai, mas a mudança no banco de dados que o disparou nunca persiste. Para corrigir isso, envolver a chamada de email em transaction.on_commit(), ou melhor, enfileirar uma task Celery dentro de on_commit().
P: Como prevenir que um signal dispare durante operações em massa?
Os métodos bulk_create(), bulk_update() e QuerySet.update() do Django não disparam signals. Isso é intencional por performance. Se o comportamento do signal for necessário, iterar e salvar individualmente (aceitando o custo de performance) ou disparar manualmente o signal após a operação em massa.
P: Uma task Celery referencia request.user. Por que ela falha?
Tasks Celery executam em um processo separado sem acesso ao contexto da requisição HTTP. Passar o ID do usuário como argumento da task e buscar o objeto User dentro da task. Nunca passar instâncias de modelos Django diretamente como argumentos de tasks, pois podem ficar desatualizadas ou falhar na serialização.
P: Como testar handlers de signal de forma isolada?
Desconectar o signal, chamar a função do handler diretamente com argumentos mock, depois reconectar. Os métodos Signal.disconnect() e Signal.connect() do Django permitem isso. O decorator @factory.django.mute_signals da biblioteca factory_boy também ajuda silenciando temporariamente signals durante a configuração de testes.
# 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:
# Chamar o handler diretamente
invalidate_product_cache(
sender=Product,
instance=product,
created=True
)
mock_cache.delete.assert_called()
finally:
# Reconectar para outros testes
post_save.connect(invalidate_product_cache, sender=Product)Antipadrões Comuns a Evitar
Cadeias de signals circulares ocorrem quando o Signal A dispara um save no Modelo B, cujo signal dispara um save no Modelo A. A recursão continua até o stack estourar ou uma guarda de recursão a parar. Projetar signals para serem terminais: observam e reagem, mas não disparam mais mudanças observáveis.
Computação pesada em signals bloqueia cada requisição que dispara o signal. Um signal que redimensiona imagens, gera miniaturas, ou chama APIs externas deveria em vez disso enfileirar uma task Celery.
Falhas silenciosas de signals escondem bugs. Por padrão, o Django captura exceções em handlers de signal e as loga, permitindo que a requisição continue. Isso mascara erros. Considerar se um handler com falha deveria abortar a operação ou realmente ser best-effort.
# settings.py
# Fazer exceções de handlers de signal se propagarem
DEBUG = True # Em dev, exceções se propagam naturalmente
# Em produção, usar Sentry ou similar para capturar erros de signal
import sentry_sdk
sentry_sdk.init(dsn="...")Comece a praticar!
Teste seus conhecimentos com nossos simuladores de entrevista e testes tecnicos.
Framework de Decisão para Tratamento de Eventos Django
Selecionar entre signals e Celery se torna simples com uma abordagem sistemática:
- Usar signals quando: a operação deve ser concluída antes da resposta, leva menos de 50ms, envolve apenas operações locais de banco de dados ou cache, e a falha deveria abortar a operação principal
- Usar Celery quando: a operação pode acontecer eventualmente, envolve serviços externos, leva mais de 100ms, se beneficia de lógica de retry, ou não deveria bloquear o usuário
- Usar signals disparando Celery quando: reconhecimento imediato é necessário mas o trabalho real é async, ou quando múltiplas tasks async devem disparar de um evento de banco de dados
- Evitar signals completamente quando: a lógica é complexa o suficiente para justificar chamadas de serviço explícitas, quando debugar cadeias de signals se torna difícil, ou quando o mesmo efeito pode ser alcançado com uma simples chamada de método
O recurso de tasks em background do Django 6.0 fornece um meio-termo: suporte de tasks async integrado sem o overhead operacional do Celery. Para projetos novos começando no final de 2026, avaliar se as tasks em background nativas do Django atendem aos requisitos antes de adicionar Celery.
Você saberia encontrar o bug em Django?
Um trecho real, um bug escondido, uma tentativa por dia. Sem conta para testar.

Escrito por
Anthony Fillion-MailletFundador da SharpSkill
Desenvolvedor fullstack há mais de 10 anos. Dirige a SharpSkill e responde por tudo o que é publicado aqui.
Atualizado em 2 de setembro de 2026
Tags
Compartilhar
Artigos relacionados

Django e Celery: Processamento Assíncrono de Tarefas e Perguntas de Entrevista 2026
Guia completo de Django e Celery com configuração, filas, Celery Beat, deploy em produção e perguntas de entrevista técnica para desenvolvedores Python em 2026.

Views assíncronas do Django e ASGI em 2026: performance e perguntas de entrevista
Uma análise aprofundada das views assíncronas do Django e do ASGI em 2026: como funcionam por baixo dos panos, qual servidor implantar, a ORM assíncrona e a armadilha do SynchronousOnlyOperation, além de perguntas de entrevista.

Django 5.2: Middleware Personalizado e Tratamento de Signals para Entrevistas Técnicas
Guia completo sobre middleware personalizado e signals no Django 5.2. Implementação de middleware de logging, middleware assíncrono, signals pre_save/post_save e perguntas frequentes de entrevista.