# 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. - Published: 2026-09-02 - Updated: 2026-09-02 - Author: Anthony Fillion-Maillet - Tags: django, celery, signals, async, python, best-practices - Reading time: 9 min --- 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. | Faktor | Django Signals | Celery Tasks | |--------|---------------|---------------| | Eksekusi | Sinkron, proses yang sama | Asinkron, proses worker | | Dampak latensi | Menambah waktu request | ~1-5ms overhead enqueue | | Penanganan kegagalan | Memecah request | Retry secara independen | | Scope transaksi | Di dalam transaksi | Di luar transaksi | | Panggilan layanan eksternal | Memblokir response | Berjalan di background | | Kompleksitas | Setup minimal | Memerlukan 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. ## 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="...") ``` ## 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. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/id/blog/django/django-signals-vs-celery-tasks-2026