Django 6.0 Tasks: Background Jobs Tanpa Celery — @task, Backends & Pitfalls
Django 6.0 punya tasks framework bawaan: @task, enqueue, dan backends. Cara kerjanya, cara migrasi dari Celery, dan pitfalls yang paling sering menggigit.
Background Jobs Akhirnya Jadi First-Class Citizen Django
Selama hampir dua dekade, jawaban untuk “gimana cara kirim email di background di Django?” selalu sama: pasang Celery. Atau RQ. Atau Django Q. Ketiganya bagus, tapi ketiganya juga bawa dunia mereka sendiri — broker, worker process, konfigurasi, dan API yang berbeda-beda. Django hanya diam di pinggir, tidak punya kontrak resmi untuk background work.
Django 6.0 mengubah itu. Sekarang ada django.tasks — tasks framework bawaan yang menstandarisasi cara kamu mendefinisikan, mengantre, dan mengecek hasil pekerjaan background. Satu decorator (@task), satu API (enqueue()), satu cara membaca status (TaskResult).
Tapi ada twist penting yang harus kamu pahami sebelum mulai: Django tidak menyediakan worker-nya. Framework ini adalah kontrak, bukan mesin. Post ini membedah cara kerjanya, menunjukkan ladder dari dev ke production, dan menyoroti tiga gotcha yang paling sering menggigit migran Celery.
Masalah yang Dipecahkan: Fragmentasi, Bukan Eksekusi
Sebelum masuk kode, luruskan dulu mental model-nya, karena ini sumber kebingungan nomor satu.
Apa yang benar-benar dikirim Django 6.0:
@taskdecorator — membungkus fungsi module-level menjadi objekTaskyang immutable.enqueue()— mengirim task ke backend, mengembalikanTaskResultdengan unique ID- Backends — dikonfigurasi via setting
TASKS, menentukan di mana task disimpan dan diambil - Task lifecycle — status
READY → RUNNING → SUCCESSFULatauFAILED
Apa yang tidak dikirim Django:
- Worker process — tidak ada. Yang menjalankan task adalah proses di luar Django
- Scheduler — tidak ada periodic/interval scheduling di core
- Broker infrastruktur — Redis/RabbitMQ tetap urusan backend pihak ketiga
Dua backend bawaan (ImmediateBackend — jalankan task inline di thread pemanggil; DummyBackend — simpan ke memori, tidak pernah eksekusi) hanya untuk development dan testing. Untuk production, kamu butuh backend pihak ketiga seperti django-tasks-db (ORM-based, menyertakan db_worker management command).
Jadi posisi Django Tasks adalah: standardisasi workflow — cara task ditulis, divalidasi, diantrekan, dan dilacak — sementara eksekusi tetap di luar scope Django. Ini keputusan desain yang masuk akal, tapi bikin banyak orang kaget.
Walkthrough: Dari Nol ke Task Pertama
Definisikan dan Enqueue
# myapp/tasks.py
from django.core.mail import send_mail
from django.tasks import task
@task
def email_users(emails, subject, message):
return send_mail(subject, message, None, emails)
@task(priority=2, queue_name="emails")
def send_welcome_email(user_id):
... # look up user, send mail
Dari mana saja — view, shell, signal handler:
result = email_users.enqueue(
emails=["user@example.com"],
subject="You have a message",
message="Hello there!",
)
print(result.id) # unique TaskResult id
Perhatikan: task function menerima argument biasa saat didefinisikan, dan kamu mempassing argument yang sama saat enqueue(). Return value task tersimpan di result.return_value.
Konfigurasi Backend: Dev vs Production
Ini bagian yang paling saya suka dari desain framework ini — perpindahan dev ke production cuma ganti satu setting:
# config/settings.py — development
TASKS = {
"default": {
"BACKEND": "django.tasks.backends.immediate.ImmediateBackend",
}
}
Untuk production dengan django-tasks-db:
# pip install django-tasks-db
INSTALLED_APPS = [
...,
"django_tasks_db", # lalu: python manage.py migrate
]
TASKS = {
"default": {
"BACKEND": "django_tasks_db.DatabaseBackend",
"QUEUES": ["default", "emails", "reports"],
}
}
Worker berjalan di proses terpisah:
python manage.py db_worker
Sama kode @task, backend berbeda. Di dev task jalan inline (praktis untuk debugging), di production task masuk tabel database dan diambil oleh db_worker.
Kamu juga bisa punya beberapa backend sekaligus dan memilihnya per task:
TASKS = {
"default": {"BACKEND": "django_tasks_db.DatabaseBackend", "QUEUES": ["default"]},
"emails": {"BACKEND": "django_tasks_db.DatabaseBackend", "QUEUES": ["emails"]},
}
# task dengan backend non-default pakai alias:
@task(backend="emails")
def send_welcome_email(user_id): ...
Cek Hasil dari View
Pattern umum: frontend enqueue task, dapatkan task ID, lalu polling status.
# myapp/views.py
from django.http import JsonResponse
from django.tasks import TaskResultStatus, default_task_backend
from myapp.tasks import send_welcome_email
def register(request):
user = create_user(request)
result = send_welcome_email.enqueue(user.id)
return JsonResponse({"task_id": str(result.id)})
def task_status(request, task_id):
result = default_task_backend.get_result(task_id)
result.refresh() # atau: await result.arefresh() di async view
if result.status == TaskResultStatus.FAILED:
return JsonResponse({
"status": "failed",
"error": result.errors[0].exception_class_path,
})
return JsonResponse({
"status": result.status.name,
"finished": result.is_finished,
})
Async API juga lengkap — aenqueue(), aget_result(), arefresh() — jadi kalau aplikasimu async-first (lihat guide migrasi async Django kita sebelumnya), tidak ada jalan buntu di sini.
Tiga Gotcha yang Paling Sering Menggigit
Bagian ini yang paling penting, terutama kalau kamu datang dari Celery.
1. Semua Lolos JSON Round-Trip
Setiap argument enqueue dan return value melewati json.dumps() / json.loads(). Konsekuensinya:
- Model instance →
TypeError. Passuser_id, bukanuser. datetimeyang bukan tz-aware →TypeError.- Tuple diam-diam berubah jadi list setelah round-trip. Kode yang mengandalkan tuple-ness akan jebol diam-diam.
Ini perbedaan terbesar dari Celery, yang mem-pickle objek Python arbitrer. “Type-stable” adalah kata kuncinya: argument yang kamu enqueue harus keluar dengan bentuk yang sama.
2. Enqueue di Dalam Transaksi yang Belum Commit
from functools import partial
from django.db import transaction
from myapp.tasks import process_order
def checkout(request):
with transaction.atomic():
order = Order.objects.create(...)
# WRONG: worker bisa menjalankan task sebelum transaksi commit
# process_order.enqueue(order_id=order.id)
# RIGHT:
transaction.on_commit(partial(process_order.enqueue, order_id=order.id))
Kalau kamu enqueue langsung di dalam transaction.atomic(), worker bisa klaim task sebelum row order tersimpan di database. Task jalan, lookup gagal, task FAILED. Klasik, dan sulit di-debug karena bersifat race condition.
3. TaskResult Adalah Snapshot, Bukan Live Object
TaskResult yang kamu terima dari enqueue() adalah snapshot pada saat itu. result.refresh() harus dipanggil untuk melihat status terbaru — di in-memory backend untuk testing ini terasa aneh, tapi di database backend ini mutlak.
Dan: membaca .return_value pada task yang belum selesai melempar ValueError. Selalu cek result.is_finished dulu. Kegagalan task tidak raise — mereka tercatat di result.errors sebagai exception_class_path plus string traceback.
Gotcha Tambahan yang Patut Dicatat
- Queue harus dideklarasikan.
queue_name="emails"tanpa"emails"diQUEUESbackend →InvalidTask. Celery tidak pernah memvalidasi nama queue, jadi setup multi-queue dari Celery perlu penyesuaian. prioritydanrun_afterbutuh backend dengansupports_priority=True/supports_defer=True. Dan ingat: task immutable — pakaitask.using(priority=10)untuk mendapat copy yang dimodifikasi, jangan mutasi langsung.db_workerauto-reload di DEBUG itu nyaman untuk dev, tapi jangan dibawa ke production.
Migrasi dari Celery: Lebih Mekanikal dari yang Dikira
Untuk kasus sederhana, migrasinya bisa dipetakan hampir satu-satu:
| Celery | Django Tasks |
|---|---|
@shared_task |
@task |
mytask.delay(args) |
mytask.enqueue(args) |
mytask.apply_async(args, priority=10) |
mytask.using(priority=10).enqueue(args) |
CELERY_* settings |
TASKS dict |
Worker: celery worker |
Worker: milik backend (mis. manage.py db_worker) |
Langkahnya: drop setting CELERY_*, install backend, ganti decorator dan pemanggilan. Yang perlu perhatian ekstra: nama queue sekarang divalidasi, jadi daftarkan semua queue di QUEUES.
Untuk periodic tasks, Django Tasks tidak menyediakan scheduler. Pakai django-crontask:
from crontask import CronTrigger, IntervalTrigger, cron
from django.tasks import task
@cron("0 0 * * *")
@task
def my_midnight_task():
print("this runs at midnight")
@cron(IntervalTrigger(hours=1))
@task
def hourly_task():
print("this runs every hour")
Tambahkan "crontask" ke INSTALLED_APPS, lalu jalankan manage.py crontask sebagai scheduler. Alternatif: system cron biasa. Dan jangan lupa housekeeping — tabel hasil task perlu di-prune (prune_db_task_results).
Trade-offs: Kapan Django Tasks Cukup, Kapan Celery Masih Menang
Django Tasks cukup kalau:
- Background work kamu sederhana: kirim email, resize gambar, panggil webhook
- Kamu mau dependency minimal — database yang sudah kamu punya sebagai queue
- Tim kamu mau satu API yang sama dari dev sampai production
- Kamu mulai proyek baru dan belum terikat infrastruktur broker
Celery masih menang kalau:
- Kamu butuh chains, chords, dan canvas workflows — komposisi task kompleks tidak ada di Django Tasks
- Throughput tinggi — broker khusus (Redis/RabbitMQ) mengungguli polling database
- Infrastruktur Celery sudah jalan dan matang — biaya migrasi tidak sebanding dengan manfaatnya
- Kamu butuh retry policy granular, routing kompleks, monitoring ekosistem (Flower dll.)
Kabar baiknya: ada usulan CeleryTaskBackend yang memungkinkan kode @task yang sama berjalan di atas Celery — jadi investasi di API Django Tasks tidak sia-sia.
Penutup: Kontrak yang Ditunggu, Bukan Pengganti Celery
Django 6.0 Tasks bukan “Celery killer” — dan itu bukan tujuannya. Yang ia berikan adalah sesuatu yang selama ini hilang: kontrak resmi untuk background work di Django. Satu cara mendefinisikan task, satu cara mengantrekan, satu cara membaca hasil — dengan implementasi yang bisa kamu tukar dari ImmediateBackend di dev ke django-tasks-db di production cukup dengan mengganti satu setting.
Kalau kamu memulai proyek Django 6.0 baru, mulailah dengan django.tasks. Kalau kamu punya Celery dengan workflow sederhana, migrasinya mekanikal dan layak dievaluasi. Kalau kamu mengandalkan canvas workflows dan throughput tinggi — Celery masih rumahmu, dan sekarang kamu punya jalan pulang yang rapi kalau kebutuhan berubah.
Sumber bacaan:
- Django 6.0 release notes
- Tasks framework overview
- Tasks API reference
- Real Python: Django Tasks
- Paul Traylor: Migrating From Celery to Django Tasks
- django-tasks-db on PyPI
Sudah coba django.tasks di Django 6.0? Atau masih stay dengan Celery? Share pengalamanmu di komentar — terutama kalau kamu menemukan gotcha lain yang tidak kami bahas di sini.
