Django Signals vs Celery Tasks 2026: Kapan Menggunakan Yang Mana

Pelajari kapan menggunakan Django signals versus Celery tasks. Membahas penanganan event sinkron vs asinkron, implikasi performa, dan pertanyaan interview untuk developer Django.

Diagram arsitektur Django signals vs Celery tasks yang menunjukkan pola penanganan event sinkron dan asinkron

Django signals dan Celery tasks keduanya menangani event dalam aplikasi Django, tetapi keduanya menyelesaikan masalah yang berbeda. Signals dieksekusi secara sinkron dalam siklus request-response, sementara Celery memindahkan pekerjaan ke background workers. Memilih tool yang salah dapat menyebabkan response yang lambat, race conditions, atau arsitektur yang terlalu kompleks.

Aturan Keputusan Cepat

Gunakan Django signals untuk side effects ringan dan sinkron yang harus selesai sebelum response. Gunakan Celery untuk apapun yang memakan waktu lebih dari 100ms, melibatkan layanan eksternal, atau dapat gagal secara independen dari request utama.

Cara Kerja Django Signals di Balik Layar

Django signals mengimplementasikan observer pattern. Ketika sebuah model disimpan, dihapus, atau ketika request dimulai atau berakhir, Django mengirimkan signal. Fungsi apapun yang terhubung ke signal tersebut dieksekusi segera, dalam transaksi database yang sama dan thread yang sama.

python
# 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):
    # Berjalan secara sinkron setelah Product.save() commit
    cache_key = f"product:{instance.id}"
    cache.delete(cache_key)
    # Juga invalidate listing kategori
    cache.delete(f"category:{instance.category_id}:products")

Signal receiver berjalan di dalam transaksi database yang sama. Jika transaksi rollback, efek signal handler tetap ada. Hal ini penting untuk cache invalidation: cache dihapus, tetapi perubahan database tidak pernah tersimpan. Hasilnya adalah cache miss yang memuat ulang data yang sudah usang.

Django 5.2 memperkenalkan transaction.on_commit() untuk mengatasi masalah ini. Membungkus logika signal dalam on_commit memastikan eksekusi hanya setelah commit berhasil:

python
# 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):
    # Tunda sampai transaksi commit berhasil
    transaction.on_commit(
        lambda: reindex_product.delay(instance.id)
    )

Pola ini menjembatani signals dan Celery: signal dijalankan secara sinkron, tetapi pekerjaan sebenarnya terjadi secara asinkron setelah transaksi commit.

Model Eksekusi Celery Task

Celery menjalankan tasks di proses worker terpisah. Sebuah Django view meng-enqueue task dengan men-serialize argumennya ke message broker (Redis atau RabbitMQ). Worker mengambil message tersebut dan mengeksekusi task secara independen dari HTTP request asli.

python
# 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):
    """Kirim email konfirmasi setelah pemesanan."""
    try:
        order = Order.objects.select_related('user').get(id=order_id)
        send_mail(
            subject=f"Order #{order.id} Dikonfirmasi",
            message=f"Pesanan Anda sebesar {order.total} telah ditempatkan.",
            from_email="orders@example.com",
            recipient_list=[order.user.email],
        )
    except Order.DoesNotExist:
        # Order dihapus sebelum task berjalan, lewati saja
        return
    except Exception as exc:
        # Retry pada kegagalan sementara (SMTP timeout, dll.)
        raise self.retry(exc=exc)

Task berjalan di proses yang berbeda, mungkin di mesin yang berbeda. Task tidak memiliki akses ke konteks request asli. Jika order dihapus antara enqueue dan eksekusi, task harus menangani hal itu dengan baik.

Celery 5.4 (versi stabil saat ini per September 2026) menambahkan peningkatan task typing dan integrasi Django yang lebih baik melalui django-celery-results untuk menyimpan hasil task di database.

Perbandingan Karakteristik Performa

Signals menambah latensi ke request. Setiap receiver yang terhubung dieksekusi sebelum response dikembalikan. Dengan tiga receiver yang rata-rata 50ms masing-masing, request memakan waktu 150ms lebih lama.

Celery menambah latensi minimal (biasanya 1-5ms untuk enqueue), tetapi memperkenalkan eventual consistency. Email dikirim "akhirnya," bukan sebelum response.

FaktorDjango SignalsCelery Tasks
EksekusiSinkron, proses yang samaAsinkron, proses worker
Dampak latensiMenambah waktu request~1-5ms overhead enqueue
Penanganan kegagalanMemecah requestRetry secara independen
Scope transaksiDi dalam transaksiDi luar transaksi
Panggilan layanan eksternalMemblokir responseBerjalan di background
KompleksitasSetup minimalMemerlukan broker + workers

Kapan Signals Adalah Pilihan yang Tepat

Signals cocok untuk skenario di mana side effect harus selesai sebelum melanjutkan dan di mana kegagalan harus membatalkan operasi.

Cache invalidation bekerja dengan baik dengan signals. Ketika Product diperbarui, representasi yang di-cache harus diinvalidasi segera. Cache yang usang bahkan untuk beberapa detik menyebabkan harga atau jumlah inventaris yang salah ditampilkan.

python
# 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}")
    # Hapus semua cache list yang mungkin menyertakan produk ini
    cache.delete_pattern(f"products:category:{instance.category_id}:*")

Audit logging di mana log harus ada sebelum response dikembalikan juga cocok untuk signals. Jika admin menghapus user, audit log harus mencatat penghapusan itu secara atomik dengan operasi delete.

Denormalisasi field di seluruh model terkait mendapat manfaat dari signals. Ketika status Order berubah menjadi "shipped," memperbarui field denormalisasi last_shipped_at pada Customer terjadi segera.

Siap menguasai wawancara Django Anda?

Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.

Kapan Celery Tasks Adalah Pilihan yang Tepat

Celery cocok untuk skenario yang melibatkan layanan eksternal, komputasi yang berjalan lama, atau operasi yang dapat gagal dan retry secara independen.

Pengiriman email tidak boleh memblokir request. Server SMTP memiliki latensi yang tidak dapat diprediksi, kadang timeout, dan membatasi rate pengirim. Celery task akan retry email yang gagal tanpa mempengaruhi user experience.

Pembuatan PDF untuk invoice atau laporan memakan waktu beberapa detik. Menghasilkan secara sinkron membuat UI terasa rusak. Enqueue task, kembalikan status "processing," dan biarkan user mengunduh ketika siap.

Panggilan API pihak ketiga termasuk dalam Celery. Memanggil Stripe, Twilio, atau layanan eksternal apapun secara sinkron menghubungkan waktu response dengan milik mereka. Ketika API mereka melambat, aplikasi juga melambat.

python
# 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):
    """Sinkronkan subscription lokal dengan state 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 membatasi rate, retry dengan exponential backoff
        raise self.retry(exc=exc)
    except Subscription.DoesNotExist:
        return  # Subscription dihapus secara lokal, tidak ada yang perlu disinkronkan

Parameter retry_backoff=True di Celery 5.x mengaktifkan exponential backoff: retry pertama setelah 1 detik, kemudian 2, kemudian 4, sampai maksimum. Ini mencegah hammering API yang rate-limited.

Pola Hybrid: Signals Memicu Tasks

Arsitektur yang paling robust menggunakan signals untuk koordinasi ringan dan segera, serta Celery untuk pekerjaan berat yang sebenarnya.

python
# 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  # Hanya tangani order baru
    
    # Enqueue semua pekerjaan async setelah transaksi commit
    transaction.on_commit(lambda: (
        send_order_confirmation.delay(instance.id),
        notify_warehouse.delay(instance.id),
        update_analytics.delay('order_created', instance.id),
    ))

Pola ini memastikan:

  • Order ada di database sebelum tasks berjalan (transaksi commit)
  • Tidak ada pekerjaan async yang terjadi jika transaksi rollback
  • HTTP response dikembalikan segera
  • Setiap task dapat gagal dan retry secara independen

Pertanyaan Interview tentang Django Signals vs Celery

Interview teknis untuk posisi Django sering mengeksplorasi perbedaan ini. Berikut adalah pertanyaan yang membedakan kandidat berpengalaman dari mereka yang hanya menghafal dokumentasi.

Q: Signal handler mengirim email. Transaksi database rollback. Apa yang terjadi?

Email tetap terkirim. Signal handlers dieksekusi selama transaksi, bukan setelah commit. Email keluar, tetapi perubahan database yang memicunya tidak pernah tersimpan. Untuk memperbaiki ini, bungkus panggilan email dalam transaction.on_commit(), atau lebih baik, enqueue Celery task di dalam on_commit().

Q: Bagaimana cara mencegah signal dari firing selama operasi bulk?

bulk_create(), bulk_update(), dan QuerySet.update() Django tidak memicu signals. Ini disengaja untuk performa. Jika perilaku signal diperlukan, iterasi dan simpan secara individual (menerima biaya performa) atau secara manual dispatch signal setelah operasi bulk.

Q: Celery task mereferensikan request.user. Mengapa gagal?

Celery tasks berjalan di proses terpisah tanpa akses ke konteks HTTP request. Kirim user ID sebagai argumen task dan ambil objek User di dalam task. Jangan pernah melewatkan instance model Django secara langsung sebagai argumen task, karena mungkin menjadi usang atau gagal di-serialize.

Q: Bagaimana cara menguji signal handlers secara terisolasi?

Disconnect signal, panggil fungsi handler secara langsung dengan argumen mock, kemudian reconnect. Method Signal.disconnect() dan Signal.connect() Django memungkinkan ini. Decorator @factory.django.mute_signals dari library factory_boy juga membantu dengan menyembunyikan signals sementara selama test setup.

python
# 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):
        # Disconnect untuk mencegah firing otomatis
        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:
                # Panggil handler secara langsung
                invalidate_product_cache(
                    sender=Product,
                    instance=product,
                    created=True
                )
                mock_cache.delete.assert_called()
        finally:
            # Reconnect untuk test lainnya
            post_save.connect(invalidate_product_cache, sender=Product)

Anti-Pattern Umum yang Harus Dihindari

Circular signal chains terjadi ketika Signal A memicu save pada Model B, yang signal-nya memicu save pada Model A. Rekursi berlanjut sampai stack overflow atau recursion guard menghentikannya. Desain signals untuk menjadi terminal: mereka mengamati dan bereaksi, tetapi tidak memicu perubahan yang dapat diamati lebih lanjut.

Komputasi berat dalam signals memblokir setiap request yang memicu signal. Signal yang mengubah ukuran gambar, menghasilkan thumbnails, atau memanggil API eksternal sebaiknya meng-enqueue Celery task.

Kegagalan signal yang diam menyembunyikan bug. Secara default, Django menangkap exception dalam signal handlers dan mencatatnya, memungkinkan request untuk melanjutkan. Ini menutupi error. Pertimbangkan apakah handler yang gagal harus membatalkan operasi atau benar-benar bersifat best-effort.

python
# settings.py
# Buat exception signal handler propagate (fail fast di development)
DEBUG = True  # Di dev, exception akan propagate secara natural

# Di production, gunakan Sentry atau serupa untuk menangkap error signal
import sentry_sdk
sentry_sdk.init(dsn="...")

Mulai berlatih!

Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.

Framework Keputusan untuk Penanganan Event Django

Memilih antara signals dan Celery menjadi mudah dengan pendekatan sistematis:

  • Gunakan signals ketika: operasi harus selesai sebelum response, memakan waktu kurang dari 50ms, hanya melibatkan operasi database atau cache lokal, dan kegagalan harus membatalkan operasi utama
  • Gunakan Celery ketika: operasi dapat terjadi akhirnya, melibatkan layanan eksternal, memakan waktu lebih dari 100ms, mendapat manfaat dari logika retry, atau tidak boleh memblokir user
  • Gunakan signals yang memicu Celery ketika: pengakuan segera diperlukan tetapi pekerjaan sebenarnya async, atau ketika beberapa async tasks harus dipicu dari satu event database
  • Hindari signals sepenuhnya ketika: logika cukup kompleks untuk menjamin panggilan service eksplisit, ketika debugging signal chains menjadi sulit, atau ketika efek yang sama dapat dicapai dengan panggilan method sederhana

Fitur background tasks Django 6.0 menyediakan jalan tengah: dukungan async task bawaan tanpa overhead operasional Celery. Untuk proyek greenfield yang dimulai di akhir 2026, evaluasi apakah background tasks native Django memenuhi kebutuhan sebelum menambahkan Celery.

Tantangan harian

Bisakah kamu menemukan bug di Django?

Satu potongan kode nyata, satu bug tersembunyi, satu percobaan per hari. Tanpa akun untuk mencoba.

Anthony Fillion-Maillet

Ditulis oleh

Anthony Fillion-Maillet

Pendiri SharpSkill

Developer fullstack selama lebih dari 10 tahun. Ia menjalankan SharpSkill dan bertanggung jawab atas semua yang diterbitkan di sini.

Diperbarui 2 September 2026

Tag

#django
#celery
#signals
#async
#python
#best-practices

Bagikan

Artikel terkait