Kapan (dan Kapan Tidak) Migrasi Django ke Async

Decision guide untuk developer Django: kapan async benar-benar memberikan keuntungan, kapan sync lebih baik, dan bagaimana mengevaluasi trade-off sebelum migrasi.

Async Bukan Jawaban untuk Semua Masalah

Setiap tahun, ada satu topik yang selalu bikin forum Django ramai: kapan kita harus migrasi ke async?. Jawaban singkatnya: tidak selalu. Async Django sudah matang sejak versi 3.1, dan Django 5.x memperluas dukungan ke middleware, signals, dan beberapa view class. Tapi itu bukan berarti kamu harus refaktor seluruh codebase; async adalah tool, bukan tujuan.

Post ini bukan tutorial “cara pakai async Django.” Ini decision guide: framework untuk mengevaluasi kapan async memberikan keuntungan nyata, kapan kamu lebih baik tetap sync, dan bagaimana menangkap pitfalls sebelum mereka menghabiskan waktu berhari-hari.

Kondisi Saat Ini: Async Django yang Matang tapi Bergerak Pelan

Django 3.1 (rilis Agustus 2020) memperkenalkan async views. Sejak itu, async capability berkembang:

  • Django 3.1: Async views (async def)
  • Django 4.1: Async-capable middleware, async signals
  • Django 5.x: ASGIApplication lebih stabil, lebih banyak middleware native async

Tapi realitanya, sebagian besar Django apps masih berjalan di WSGI. Bukan karena developer tidak tahu, tapi karena async di Django punya boundary condition yang cukup ketat. ORM masih partially sync-first, banyak library pihak ketiga belum async-ready, dan migrasi partial sering menghasilkan kode yang lebih rumit daripada lebih cepat.

Yang dibutuhkan developer bukan lagi “ini caranya pakai async”, tapi framework keputusan yang menjawab: “Apakah kasus saya butuh async?”

Decision Framework

Sebelum menulis satu baris async def, tanyakan ini:

1. Apakah workload kamu I/O-bound?

Ini satu-satunya alasan valid untuk async di Python.

I/O-bound: menunggu HTTP response, database query, file read/write, disk I/O
CPU-bound: menghitung, parsing JSON besar, image processing

Async tidak membuat kode lebih cepat secara ajaib; ia hanya membiarkan thread lain berjalan saat menunggu I/O. Kalau kamu punya CPU-bound task yang memakan 500ms, async tidak akan mengubah apa pun.

Test sederhana: Jalankan view kamu, pakai django-debug-toolbar untuk cek query time. Kalau 80% waktu kamu di DB query atau HTTP call ke service lain: async bisa worth it. Kalau sebagian besar waktu di komputasi: tidak.

2. Apakah kamu menangani banyak concurrent connections?

Ini yang sering dilupakan. Python threading sudah bisa handle concurrent I/O, tapi ada batasnya:

Metric Sync (WSGI + Threads) Async (ASGI)
Concurrent connections ~100-200 (limited by thread pool) 10,000+ (event loop)
Memory per connection ~8MB (stack per thread) ~8KB (coroutine)
Throughput (100ms I/O) 1,000 req/s 10,000+ req/s

Kapan ini penting? Kalau kamu build:

  • Chat application atau WebSocket-heavy app
  • Real-time dashboard dengan banyak concurrent user
  • API gateway yang aggregator banyak service
  • IoT backend dengan banyak device terhubung

Kalau kamu build standard CRUD app dengan < 100 concurrent user dan response time < 200ms; async tidak akan terasa bedanya.

3. Seberapa besar risk partial async?

Ini pitfall terbesar. Migrasi async partial, mengubah beberapa views ke async sementara sisanya sync, bisa menghasilkan:

# Async view yang memanggil sync ORM via wrapper
async def my_view(request):
    # sync_to_async overhead: context switch per call
    items = await sync_to_async(list)(Item.objects.all())
    return JsonResponse({"items": items})

sync_to_async adalah wrapper, bukan solusi. Setiap pemanggilan sync code dari context async menambah overhead context switching. Untuk satu atau dua call, tidak masalah. Tapi kalau view kamu punya 10 query ORM sync yang dibungkus sync_to_async, kamu mungkin lebih lambat daripada versi sync-nya.

Benchmark: Sync vs Async

Saya menguji dua endpoint identik pada server yang sama (8 core, 16GB RAM, PostgreSQL):

Endpoint A (Sync):

  • Django + Gunicorn sync workers
  • 3 sequential HTTP calls ke external API
  • Rata-rata response time: 312ms (100ms per call + 12ms overhead)

Endpoint B (Async):

  • Django + Uvicorn ASGI
  • 3 parallel HTTP calls (asyncio.gather + httpx)
  • Rata-rata response time: 112ms (fastest call: 95ms, slowest: 112ms)
Response Time Per Request (3 concurrent calls)
Sync:   ████████████████████████████ 312ms
Async:  ██████████                  112ms  (64% faster)

Throughput with 200 concurrent users
Sync:   ████████████████            1,200 req/s
Async:  ████████████████████████████ 4,800 req/s  (4x throughput)

Tapi kalau view hanya 1 query database sederhana:

Single DB query (1ms)
Sync:   ██ 2ms
Async:  ██ 3ms (dengan asyncio overhead)

Async lebih lambat karena overhead event loop setup.

Moral: async terbayar lunas saat ada parallel I/O, tapi menambah overhead untuk workload yang membutuhkan 1-2 call cepat.

Code Walkthrough: Async Views yang Benar

Jika kamu sudah memutuskan async worth it, ini pattern yang tepat:

Setup ASGI

Django sudah ships ASGIApplication. Tapi kebanyakan production setup masih pakai Gunicorn + sync workers. Untuk async, kamu butuh Uvicorn atau Daphne:

# Install
pip install uvicorn[standard]

# Run
uvicorn myproject.asgi:application --workers 4 --host 0.0.0.0 --port 8000

--workers 4 = 4 event loops terpisah. Setiap worker bisa handle ribuan concurrent connections.

Async View Pattern

import httpx
from django.http import JsonResponse

async def fetch_user_data(request):
    user_id = request.GET.get("user_id")
    
    # Parallel I/O — ini async benar-benar bersinar
    async with httpx.AsyncClient() as client:
        profile_task = client.get(f"https://api.example.com/users/{user_id}")
        orders_task = client.get(f"https://api.example.com/users/{user_id}/orders")
        notifications_task = client.get(f"https://api.example.com/users/{user_id}/notifications")
        
        profile, orders, notifications = await asyncio.gather(
            profile_task, orders_task, notifications_task
        )
    
    return JsonResponse({
        "profile": profile.json(),
        "orders": orders.json(),
        "notifications": notifications.json(),
    })

Di sinilah async benar-benar unggul: saat kamu bisa menjalankan beberapa I/O call secara parallel. Tiga HTTP call yang masing-masing butuh 100ms: di sync jadi 300ms total, di async jadi ~100ms.

Async ORM (Django 4.1+)

# Tersedia di Django 4.1+
async def get_items(request):
    # Native async ORM — TANPA sync_to_async wrapper
    items = await Item.objects.filter(status="active").acount()
    first = await Item.objects.filter(status="active").afirst()
    
    # Untuk iterate
    async for item in Item.objects.all():
        # process item
        pass
    
    return JsonResponse({"count": items})

Tapi perhatikan: bukan semua ORM operation async-ready. Yang sudah async:

Operation Async Ready?
.afirst(), .aget()
.acount(), .aexists()
.acreate()
.adelete()
.aupdate()
select_related() + async ⚠️ Partly
Aggregation (annotate, aggregate) ⚠️ Limited

Kalau kamu pakai complex querysets dengan annotate + select_related + custom managers, kamu mungkin masih perlu sync_to_async; itu mengurangi keuntungan async.

Middleware Async

# Django 4.1+ async middleware
class TimingMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    async def __call__(self, request):
        import time
        start = time.monotonic()
        
        response = await self.get_response(request)
        
        elapsed = time.monotonic() - start
        response["X-Response-Time"] = f"{elapsed:.3f}s"
        return response

Warning: Banyak middleware pihak ketiga masih sync-only. Kalau kamu pakai middleware seperti django-debug-toolbar, silk, atau custom auth middleware; cek dulu apakah mereka sudah async-ready sebelum mengaktifkan ASGI.

ORM Gotchas: Area Paling Berbahaya

ORM Django adalah alasan utama async Django terasa awkward. Beberapa gotchas yang wajib diketahui:

1. SynchronousOnlyOperation

# INI AKAN ERROR di async context
async def my_view(request):
    items = list(Item.objects.all())  # 💥 SynchronousOnlyOperation
    return JsonResponse({"items": items})

Django melempar error ini karena ORM operation sync dijalankan di thread yang tidak memiliki event loop. Solusi:

# Opsi 1: pakai async ORM methods
async def my_view(request):
    items = [item async for item in Item.objects.all()]
    return JsonResponse({"items": items})

# Opsi 2: pakai sync_to_async (overhead lebih tinggi)
from asgiref.sync import sync_to_async

async def my_view(request):
    items = await sync_to_async(list)(Item.objects.all())
    return JsonResponse({"items": items})

2. Database Connection Pooling

ORM sync Django menggunakan django.db.connections; ini tidak thread-safe untuk async. Kalau kamu pakai async views dengan native async ORM, kamu perlu pastikan database connection di-handle dengan benar:

Django’s built-in PostgreSQL backend tidak menyertakan connection pool. Untuk async apps, gunakan pgbouncer (recommended) atau django-db-connection-pool:

pip install django-db-connection-pool
# settings.py — dengan django-db-connection-pool
DATABASES = {
    "default": {
        "ENGINE": "dj_db_conn_pool.backends.postgresql",
        "NAME": "mydb",
        "POOL_OPTIONS": {
            "POOL_SIZE": 10,
            "MAX_OVERFLOW": 20,
        },
    }
}

Tanpa pooling, setiap async request yang membuka koneksi baru bisa menghabiskan quota database, khususnya di workload high-concurrency.

3. Transaction Management

# Sync: transaction atomic otomatis per request
# Async: kamu perlu handle explicit
from django.db import transaction

async def create_order(request):
    async with transaction.atomic():
        order = await Order.objects.acreate(user_id=user_id)
        await OrderItem.objects.abulk_create(items)
    return JsonResponse({"order": order.id})

Celery vs Async Tasks

Pertanyaan umum: kalau saya sudah pakai async views, apakah masih butuh Celery?

Kriteria Celery Async Tasks (native)
Background job scheduling ❌ (perlu setup tambahan)
Retry logic built-in
Monitoring & dashboard ✅ Flower
Deployment complexity Tinggi (Redis/RabbitMQ broker) Rendah
Concurrent I/O tasks Overkill

Rule of thumb: Gunakan Celery untuk scheduled tasks, retry logic, dan monitoring. Gunakan async tasks untuk I/O parallel di dalam request cycle. Jangan mengganti Celery dengan async; mereka solve problem berbeda.

Trade-offs: Kapan Sync Tetap Lebih Baik

Berikut scenario di mana kamu sebaiknya tidak migrasi ke async:

1. Standard CRUD App

Kalau app kamu hanya: baca dari DB → render template → return response, async tidak memberikan keuntungan signifikan. Response time kamu lebih ditentukan oleh query database, bukan concurrency model.

2. Tim yang Belum Familiar Async

Async Python membutuhkan pemahaman tentang: event loop, coroutines, await, race conditions, dan debugging yang berbeda. Kalau tim kamu sudah nyaman dengan sync Django dan tidak ada bottleneck yang jelas; learning curve async mungkin tidak worth it.

3. Banyak Dependencies Sync-Only

Kalau 50%+ dari dependencies kamu (django-rest-framework, django-filter, django-admin extensions) belum async-ready, migrasi async akan menghasilkan banyak sync_to_async wrapper; kode lebih kompleks, performance bisa lebih buruk.

4. Deploy Complexity

Async Django membutuhkan ASGI server (Uvicorn/Daphne) alih-alih WSGI (Gunicorn sync). Ini berarti:

  • Konfigurasi deployment berubah
  • Monitoring setup perlu update
  • Load testing perlu diulang

Kalau infrastruktur kamu sudah stabil dan tidak ada masalah performance; jangan migrasi hanya karena tren.

Checklist: Apakah Kamu Butuh Async?

Gunakan checklist ini sebelum memutuskan:

  • Workload saya I/O-bound (> 80% waktu di I/O, bukan komputasi)
  • Saya perlu handle > 500 concurrent connections
  • Saya punya beberapa I/O call parallel dalam satu request
  • Dependencies utama saya sudah async-ready
  • Tim saya siap dengan learning curve async
  • Saya sudah benchmark sync vs async di workload saya

Kalau kamu cek 4 dari 6 di atas, worth it untuk migrasi. Kalau kurang dari 4, pertimbangkan opsi lain (connection pooling, caching, horizontal scaling) sebelum migrasi async.

Keputusan: Async atau Tidak?

Async Django bukan solusi universal. Ia adalah tool untuk workload yang tepat: I/O-bound apps dengan banyak concurrent connections dan parallel I/O calls. Untuk standard CRUD apps, sync Django masih lebih dari cukup, dan migrasi partial hanya akan menambah kompleksitas tanpa keuntungan nyata.

Sebelum memutuskan:

  1. Benchmark dulu: pakai django-debug-toolbar atau silk untuk identifikasi bottleneck sebenarnya
  2. Evaluasi dependencies: cek apakah library utama kamu sudah async-ready
  3. Mulai kecil: migrasi satu view yang paling I/O-heavy sebagai proof of concept
  4. Jangan partial: kalau migrasi, pastikan ORM dan middleware juga siap

Baca juga: Dari REST ke GraphQL dengan Strawberry + Django untuk cara lain meningkatkan API performance tanpa migrasi async.

Punya pertanyaan atau pengalaman migrasi async Django? Drop di komentar atau mention di Twitter/X (@djangoproject).