Django Signals vs Celery Tasks 2026: เมื่อไหร่ควรใช้อะไร

เรียนรู้ว่าเมื่อไหร่ควรใช้ Django signals เทียบกับ Celery tasks ครอบคลุมการจัดการ event แบบ synchronous vs asynchronous ผลกระทบด้านประสิทธิภาพ และคำถามสัมภาษณ์สำหรับนักพัฒนา Django

ไดอะแกรมสถาปัตยกรรม Django signals vs Celery tasks แสดงรูปแบบการจัดการ event แบบ synchronous และ asynchronous

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 SignalsCelery Tasks
การทำงานSynchronous กระบวนการเดียวAsynchronous กระบวนการ worker
ผลกระทบ latencyเพิ่มเวลา request~1-5ms overhead enqueue
การจัดการความล้มเหลวทำลาย requestRetry อย่างอิสระ
ขอบเขต 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 เกิดขึ้นทันที

พร้อมที่จะพิชิตการสัมภาษณ์ Django แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

เมื่อไหร่ที่ 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

ชาเลนจ์ประจำวัน

คุณหาบั๊กใน Django เจอไหม

โค้ดจริงหนึ่งชิ้น บั๊กที่ซ่อนอยู่หนึ่งจุด วันละหนึ่งครั้ง ลองได้โดยไม่ต้องมีบัญชี

Anthony Fillion-Maillet

เขียนโดย

Anthony Fillion-Maillet

ผู้ก่อตั้ง SharpSkill

เป็นนักพัฒนาฟูลสแตกมากว่า 10 ปี ดูแล SharpSkill และรับผิดชอบทุกสิ่งที่เผยแพร่ที่นี่

อัปเดตเมื่อ 2 กันยายน 2569

แท็ก

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

แชร์

บทความที่เกี่ยวข้อง