# Django Signals vs Celery Tasks 2026: เมื่อไหร่ควรใช้อะไร > เรียนรู้ว่าเมื่อไหร่ควรใช้ Django signals เทียบกับ Celery tasks ครอบคลุมการจัดการ event แบบ synchronous vs asynchronous ผลกระทบด้านประสิทธิภาพ และคำถามสัมภาษณ์สำหรับนักพัฒนา 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 และ Celery tasks ทั้งคู่จัดการ events ในแอปพลิเคชัน Django แต่ทั้งสองแก้ปัญหาที่แตกต่างกัน Signals ทำงานแบบ synchronous ภายใน request-response cycle ในขณะที่ Celery ส่งงานไปยัง background workers การเลือกเครื่องมือที่ผิดนำไปสู่ response ที่ช้า race conditions หรือสถาปัตยกรรมที่ซับซ้อนเกินไป > **กฎการตัดสินใจอย่างรวดเร็ว** > > ใช้ Django signals สำหรับ side effects ที่เบาและ synchronous ที่ต้องเสร็จก่อน response ใช้ Celery สำหรับทุกอย่างที่ใช้เวลามากกว่า 100ms เกี่ยวข้องกับ external services หรือสามารถล้มเหลวได้อย่างอิสระจาก request หลัก ## วิธีการทำงานของ Django Signals เบื้องหลัง Django signals ใช้ observer pattern เมื่อ model ถูก save ลบ หรือเมื่อ request เริ่มต้นหรือสิ้นสุด Django จะส่ง signal ออกไป ฟังก์ชันใดๆ ที่เชื่อมต่อกับ signal นั้นจะทำงานทันที ใน database transaction เดียวกันและ thread เดียวกัน ```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): # ทำงานแบบ synchronous หลังจาก Product.save() commit cache_key = f"product:{instance.id}" cache.delete(cache_key) # ยัง invalidate category listing ด้วย cache.delete(f"category:{instance.category_id}:products") ``` Signal receiver ทำงานภายใน database transaction เดียวกัน ถ้า transaction rollback ผลของ signal handler ยังคงอยู่ สิ่งนี้สำคัญสำหรับ cache invalidation: cache ถูกลบ แต่การเปลี่ยนแปลง database ไม่เคยถูกบันทึก ผลลัพธ์คือ cache miss ที่โหลดข้อมูลเก่ากลับมา Django 5.2 แนะนำ `transaction.on_commit()` เพื่อแก้ปัญหานี้ การห่อ logic ของ signal ใน `on_commit` รับประกันการทำงานหลังจาก commit สำเร็จเท่านั้น: ```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): # รอจนกว่า transaction จะ commit สำเร็จ transaction.on_commit( lambda: reindex_product.delay(instance.id) ) ``` รูปแบบนี้เชื่อมต่อ signals และ Celery: signal ถูกเรียกใช้แบบ synchronous แต่งานจริงเกิดขึ้นแบบ asynchronous หลังจาก transaction commit ## โมเดลการทำงานของ Celery Task Celery รัน tasks ในกระบวนการ worker แยกต่างหาก Django view จะ enqueue task โดย serialize arguments ไปยัง message broker (Redis หรือ RabbitMQ) Worker จะรับ message และทำ task โดยไม่ขึ้นกับ HTTP request เดิม ```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): """ส่งอีเมลยืนยันหลังจากสั่งซื้อ""" try: order = Order.objects.select_related('user').get(id=order_id) send_mail( subject=f"คำสั่งซื้อ #{order.id} ได้รับการยืนยัน", message=f"คำสั่งซื้อของคุณมูลค่า {order.total} ได้ถูกวางแล้ว", from_email="orders@example.com", recipient_list=[order.user.email], ) except Order.DoesNotExist: # Order ถูกลบก่อนที่ task จะทำงาน ข้ามไป return except Exception as exc: # Retry เมื่อเกิดความล้มเหลวชั่วคราว (SMTP timeout เป็นต้น) raise self.retry(exc=exc) ``` Task ทำงานในกระบวนการที่แตกต่าง อาจจะอยู่บนเครื่องที่แตกต่าง มันไม่มีสิทธิ์เข้าถึง context ของ request เดิม ถ้า order ถูกลบระหว่าง enqueue และการทำงาน task ต้องจัดการสิ่งนั้นอย่างเหมาะสม Celery 5.4 (เวอร์ชันเสถียรปัจจุบัน ณ กันยายน 2026) เพิ่มการปรับปรุง task typing และการรวม Django ที่ดีขึ้นผ่าน `django-celery-results` สำหรับเก็บผลลัพธ์ task ใน database ## เปรียบเทียบคุณลักษณะด้านประสิทธิภาพ Signals เพิ่ม latency ให้กับ request ทุก receiver ที่เชื่อมต่อจะทำงานก่อนที่ response จะถูกส่งกลับ ด้วย receivers สามตัวที่เฉลี่ย 50ms แต่ละตัว request จะใช้เวลาเพิ่มขึ้น 150ms Celery เพิ่ม latency น้อยมาก (ปกติ 1-5ms สำหรับ enqueue) แต่แนะนำ eventual consistency อีเมลจะถูกส่ง "ในที่สุด" ไม่ใช่ก่อน response | ปัจจัย | Django Signals | Celery Tasks | |--------|---------------|---------------| | การทำงาน | Synchronous กระบวนการเดียว | Asynchronous กระบวนการ worker | | ผลกระทบ latency | เพิ่มเวลา request | ~1-5ms overhead enqueue | | การจัดการความล้มเหลว | ทำลาย request | Retry อย่างอิสระ | | ขอบเขต transaction | ภายใน transaction | ภายนอก transaction | | การเรียก external services | บล็อก response | ทำงานใน background | | ความซับซ้อน | Setup น้อยที่สุด | ต้องการ broker + workers | ## เมื่อไหร่ที่ Signals เป็นทางเลือกที่ถูกต้อง Signals เหมาะกับสถานการณ์ที่ side effect ต้องเสร็จก่อนดำเนินการต่อ และเมื่อความล้มเหลวควรยกเลิกการดำเนินการ **Cache invalidation** ทำงานได้ดีกับ signals เมื่อ Product ถูกอัพเดท การแสดงที่ถูก cache ต้องถูก invalidate ทันที cache ที่เก่าแม้เพียงไม่กี่วินาทีทำให้แสดงราคาหรือจำนวนสินค้าคงคลังที่ไม่ถูกต้อง ```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}") # ลบ cache รายการใดๆ ที่อาจรวมผลิตภัณฑ์นี้ cache.delete_pattern(f"products:category:{instance.category_id}:*") ``` **Audit logging** ที่ log ต้องมีอยู่ก่อนที่ response จะถูกส่งกลับก็เหมาะกับ signals ถ้า admin ลบ user audit log ควรบันทึกการลบนั้นอย่าง atomic กับการดำเนินการลบ **Denormalization** ของ fields ข้าม models ที่เกี่ยวข้องได้ประโยชน์จาก signals เมื่อสถานะ Order เปลี่ยนเป็น "shipped" การอัพเดท field denormalized `last_shipped_at` บน Customer เกิดขึ้นทันที ## เมื่อไหร่ที่ Celery Tasks เป็นทางเลือกที่ถูกต้อง Celery เหมาะกับสถานการณ์ที่เกี่ยวข้องกับ external services การคำนวณที่ใช้เวลานาน หรือการดำเนินการที่สามารถล้มเหลวและ retry ได้อย่างอิสระ **การส่งอีเมล** ไม่ควรบล็อก requests เซิร์ฟเวอร์ SMTP มี latency ที่ไม่สามารถคาดเดาได้ บางครั้ง timeout และจำกัด rate ผู้ส่ง Celery task จะ retry อีเมลที่ล้มเหลวโดยไม่ส่งผลกระทบต่อประสบการณ์ผู้ใช้ **การสร้าง PDF** สำหรับใบแจ้งหนี้หรือรายงานใช้เวลาหลายวินาที การสร้างแบบ synchronous ทำให้ UI รู้สึกเสีย Enqueue task คืนสถานะ "processing" และให้ผู้ใช้ดาวน์โหลดเมื่อพร้อม **การเรียก API บุคคลที่สาม** อยู่ใน Celery การเรียก Stripe Twilio หรือ external service ใดๆ แบบ synchronous เชื่อมโยงเวลา response กับของพวกเขา เมื่อ API ของพวกเขาช้าลง แอปพลิเคชันก็ช้าลง ```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): """ซิงค์ subscription ในเครื่องกับสถานะ 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 จำกัด rate retry ด้วย exponential backoff raise self.retry(exc=exc) except Subscription.DoesNotExist: return # Subscription ถูกลบในเครื่อง ไม่มีอะไรต้องซิงค์ ``` พารามิเตอร์ `retry_backoff=True` ใน Celery 5.x เปิดใช้งาน exponential backoff: retry แรกหลังจาก 1 วินาที จากนั้น 2 จากนั้น 4 จนถึงสูงสุด สิ่งนี้ป้องกันการ hammering API ที่ถูก rate-limited ## รูปแบบ Hybrid: Signals ทริกเกอร์ Tasks สถาปัตยกรรมที่แข็งแกร่งที่สุดใช้ signals สำหรับการประสานงานเบาและทันที และ Celery สำหรับงานหนักจริงๆ ```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 # จัดการเฉพาะ orders ใหม่เท่านั้น # Enqueue งาน async ทั้งหมดหลังจาก transaction commit transaction.on_commit(lambda: ( send_order_confirmation.delay(instance.id), notify_warehouse.delay(instance.id), update_analytics.delay('order_created', instance.id), )) ``` รูปแบบนี้รับประกัน: - Order มีอยู่ใน database ก่อนที่ tasks จะทำงาน (transaction commit แล้ว) - ไม่มีงาน async เกิดขึ้นถ้า transaction rollback - HTTP response ถูกส่งกลับทันที - แต่ละ task สามารถล้มเหลวและ retry ได้อย่างอิสระ ## คำถามสัมภาษณ์เกี่ยวกับ Django Signals vs Celery การสัมภาษณ์ทางเทคนิคสำหรับตำแหน่ง Django มักสำรวจความแตกต่างนี้ นี่คือคำถามที่แยกผู้สมัครที่มีประสบการณ์ออกจากผู้ที่แค่ท่องจำเอกสาร **Q: Signal handler ส่งอีเมล Transaction database rollback เกิดอะไรขึ้น?** อีเมลถูกส่งอยู่ดี Signal handlers ทำงานระหว่าง transaction ไม่ใช่หลังจาก commit อีเมลออกไป แต่การเปลี่ยนแปลง database ที่ทริกเกอร์มันไม่เคยถูกบันทึก เพื่อแก้ไขสิ่งนี้ ห่อการเรียกอีเมลใน `transaction.on_commit()` หรือดีกว่า enqueue Celery task ภายใน `on_commit()` **Q: จะป้องกัน signal จากการ firing ระหว่างการดำเนินการ bulk ได้อย่างไร?** `bulk_create()` `bulk_update()` และ `QuerySet.update()` ของ Django ไม่ทริกเกอร์ signals นี่เป็นความตั้งใจเพื่อประสิทธิภาพ ถ้าต้องการพฤติกรรม signal ให้ iterate และ save ทีละตัว (ยอมรับต้นทุนประสิทธิภาพ) หรือ dispatch signal ด้วยตนเองหลังจากการดำเนินการ bulk **Q: Celery task อ้างอิง `request.user` ทำไมมันล้มเหลว?** Celery tasks ทำงานในกระบวนการแยกต่างหากโดยไม่มีสิทธิ์เข้าถึง context ของ HTTP request ส่ง user ID เป็น argument ของ task และดึง object User ภายใน task อย่าส่ง instance ของ Django model โดยตรงเป็น arguments ของ task เพราะอาจล้าสมัยหรือไม่สามารถ serialize ได้ **Q: จะทดสอบ signal handlers แบบแยกได้อย่างไร?** Disconnect signal เรียกฟังก์ชัน handler โดยตรงด้วย mock arguments จากนั้น reconnect เมธอด `Signal.disconnect()` และ `Signal.connect()` ของ Django อนุญาตสิ่งนี้ Decorator `@factory.django.mute_signals` จากไลบรารี `factory_boy` ก็ช่วยโดยการปิดเสียง signals ชั่วคราวระหว่าง 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 เพื่อป้องกันการ firing อัตโนมัติ 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: # เรียก handler โดยตรง invalidate_product_cache( sender=Product, instance=product, created=True ) mock_cache.delete.assert_called() finally: # Reconnect สำหรับ tests อื่นๆ post_save.connect(invalidate_product_cache, sender=Product) ``` ## Anti-Patterns ทั่วไปที่ควรหลีกเลี่ยง **Circular signal chains** เกิดขึ้นเมื่อ Signal A ทริกเกอร์ save บน Model B ซึ่ง signal ของมันทริกเกอร์ save บน Model A recursion ดำเนินต่อไปจนกว่า stack จะ overflow หรือ recursion guard หยุดมัน ออกแบบ signals ให้เป็น terminal: พวกมันสังเกตและตอบสนอง แต่ไม่ทริกเกอร์การเปลี่ยนแปลงที่สังเกตได้เพิ่มเติม **การคำนวณหนักใน signals** บล็อกทุก request ที่ทริกเกอร์ signal Signal ที่ resize รูปภาพ สร้าง thumbnails หรือเรียก external APIs ควร enqueue Celery task แทน **ความล้มเหลวของ signal ที่เงียบ** ซ่อน bugs โดยค่าเริ่มต้น Django จับ exceptions ใน signal handlers และบันทึกพวกมัน อนุญาตให้ request ดำเนินต่อ สิ่งนี้ปกปิดข้อผิดพลาด พิจารณาว่า handler ที่ล้มเหลวควรยกเลิกการดำเนินการหรือเป็น best-effort จริงๆ ```python # settings.py # ทำให้ exception ของ signal handler propagate (fail fast ใน development) DEBUG = True # ใน dev exceptions จะ propagate โดยธรรมชาติ # ใน production ใช้ Sentry หรือคล้ายกันเพื่อจับ signal errors import sentry_sdk sentry_sdk.init(dsn="...") ``` ## กรอบการตัดสินใจสำหรับการจัดการ Event ของ Django การเลือกระหว่าง signals และ Celery กลายเป็นเรื่องง่ายด้วยแนวทางที่เป็นระบบ: - **ใช้ signals เมื่อ**: การดำเนินการต้องเสร็จก่อน response ใช้เวลาน้อยกว่า 50ms เกี่ยวข้องเฉพาะการดำเนินการ database หรือ cache ในเครื่อง และความล้มเหลวควรยกเลิกการดำเนินการหลัก - **ใช้ Celery เมื่อ**: การดำเนินการสามารถเกิดขึ้นในที่สุด เกี่ยวข้องกับ external services ใช้เวลามากกว่า 100ms ได้ประโยชน์จาก retry logic หรือไม่ควรบล็อกผู้ใช้ - **ใช้ signals ที่ทริกเกอร์ Celery เมื่อ**: ต้องการการยืนยันทันทีแต่งานจริงเป็น async หรือเมื่อ async tasks หลายตัวควรถูก fire จาก database event เดียว - **หลีกเลี่ยง signals ทั้งหมดเมื่อ**: logic ซับซ้อนพอที่จะรับประกันการเรียก service ที่ชัดเจน เมื่อการ debugging signal chains กลายเป็นเรื่องยาก หรือเมื่อผลเดียวกันสามารถทำได้ด้วยการเรียก method ง่ายๆ ฟีเจอร์ background tasks ของ Django 6.0 มอบทางเลือกกลาง: รองรับ async task ในตัวโดยไม่มี overhead การดำเนินงานของ Celery สำหรับโปรเจ็กต์ greenfield ที่เริ่มต้นในปลายปี 2026 ให้ประเมินว่า background tasks ดั้งเดิมของ Django ตรงตามข้อกำหนดหรือไม่ก่อนที่จะเพิ่ม Celery --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/th/blog/django/django-signals-vs-celery-tasks-2026