Jika akses ke model AI hanya dibuka untuk trusted partners, mahal per request, dan terkena batas rate limit vendor, pola request-response sinkron biasa sering tidak cukup. Solusi yang lebih aman adalah memasukkan pekerjaan ke queue, memprosesnya lewat worker async, menyimpan hasil yang sering diminta di cache, dan memakai distributed lock agar pekerjaan yang sama tidak dikerjakan berulang.

Desain ini relevan ketika ada lonjakan traffic setelah rilis akses model baru ke partner tertentu. Fokusnya bukan pada beritanya, melainkan pada konsekuensi teknis di backend: request mahal, timeout upstream, hasil tidak selalu langsung tersedia, dan kebutuhan menjaga sistem tetap stabil walau vendor lambat atau membatasi throughput.

Kapan pola ini dibutuhkan

Desain worker queue untuk akses model AI dengan cache dan lock cocok saat Anda menghadapi satu atau beberapa kondisi berikut:

  • Biaya inference tinggi, sehingga request duplikat perlu ditekan.
  • Rate limit vendor ketat, misalnya jumlah request per menit atau concurrency dibatasi.
  • Latency tidak stabil, karena model AI bisa merespons dalam hitungan detik atau lebih lama.
  • Hasil tidak harus sinkron, misalnya pengguna bisa menunggu status job atau melakukan polling.
  • Prompt populer berulang, sehingga cache bisa memberi penghematan nyata.
  • Traffic bursty, misalnya setelah produk diluncurkan ke partner atau tenant baru.

Tanpa queue, aplikasi akan cenderung meneruskan lonjakan traffic langsung ke vendor model. Akibatnya throughput tidak rata, timeout meningkat, dan biaya bisa membengkak karena prompt yang sama dihitung berkali-kali.

Alur arsitektur yang disarankan

Komponen utama

  • API service: menerima request klien, validasi, menghasilkan idempotency key, cek cache, lalu enqueue job.
  • Queue broker: menyimpan job yang menunggu diproses.
  • Worker: mengambil job, mengambil lock, memanggil vendor model AI, lalu menyimpan hasil.
  • Cache: menyimpan hasil inference untuk prompt umum atau hasil yang masih valid.
  • Data store: menyimpan status job, metadata, dan hasil final jika perlu persistensi lebih lama.
  • Distributed lock: mencegah dua worker mengerjakan unit kerja yang sama secara bersamaan.
  • DLQ (dead-letter queue): menampung job yang gagal permanen atau melebihi batas retry.
  • Observability stack: metrics, log terstruktur, dan tracing.

Alur request

  1. Klien mengirim request inferensi.
  2. API menormalisasi input yang relevan untuk cache key dan idempotency key.
  3. API mengecek cache. Jika ada cache hit yang masih valid, hasil dikembalikan langsung.
  4. Jika tidak ada, API mengecek apakah job identik sedang berjalan menggunakan idempotency key atau status store.
  5. Jika job belum ada, API membuat record job berstatus queued dan memasukkannya ke queue.
  6. Worker mengambil job, memperoleh distributed lock berdasarkan kunci pekerjaan.
  7. Worker memanggil vendor model AI dengan timeout yang jelas.
  8. Jika sukses, worker menyimpan hasil ke database dan cache, lalu menandai job completed.
  9. Jika gagal sementara, worker melakukan retry dengan backoff.
  10. Jika gagal permanen atau retry habis, job dipindahkan ke DLQ.

Prinsip utamanya: queue meratakan beban, cache mengurangi request mahal, dan lock mencegah duplicate work.

Desain data: job, key, dan status

Idempotency key

Idempotency key dipakai agar request yang sama tidak menghasilkan banyak job saat klien retry atau jaringan tidak stabil. Kunci ini sebaiknya dibentuk dari kombinasi yang benar-benar mendefinisikan unit kerja, misalnya:

  • tenant atau partner ID,
  • model target,
  • versi parameter penting,
  • hasil normalisasi prompt/input,
  • opsional: scope waktu jika hasil memang boleh berubah.

Jangan memasukkan field yang tidak relevan seperti timestamp request mentah, karena itu membuat request identik dianggap berbeda.

Cache key

Cache key mirip dengan idempotency key, tetapi tujuan utamanya adalah pengambilan hasil. Kadang cache key perlu lebih ketat atau lebih longgar tergantung kebutuhan konsistensi. Misalnya, jika hasil dipengaruhi locale atau safety profile, dua hal itu harus masuk ke key.

Status job

Gunakan state yang eksplisit agar debugging lebih mudah:

  • queued
  • processing
  • completed
  • retrying
  • failed
  • dead_lettered

Simpan juga metadata seperti jumlah percobaan, waktu enqueue, waktu mulai proses, error terakhir, latency upstream, dan vendor request ID jika tersedia.

Peran cache: menghemat biaya dan menurunkan latency

Apa yang layak di-cache

Tidak semua hasil model AI layak di-cache. Cache paling berguna untuk:

  • prompt umum yang sering berulang,
  • hasil deterministik atau relatif stabil,
  • transformasi yang tidak sangat personal,
  • hasil intermediate yang mahal tetapi bisa dipakai ulang.

Cache kurang cocok jika output sangat bergantung pada konteks pengguna per request, atau jika freshness harus sangat tinggi.

TTL dan freshness

TTL harus mengikuti karakter data, bukan angka acak. Untuk hasil yang cukup stabil, TTL bisa lebih panjang. Untuk output yang sensitif terhadap perubahan policy, konteks, atau data referensi, TTL perlu lebih pendek.

Pola yang umum dipakai:

  • Hard TTL: setelah lewat TTL, hasil dianggap tidak valid.
  • Soft TTL: hasil boleh tetap disajikan sebentar sambil refresh dilakukan di belakang layar.
  • Stale-while-revalidate: mengurangi lonjakan miss serentak saat item populer kedaluwarsa.

Masalah cache stale

Cache stale adalah trade-off yang sengaja diambil demi performa dan biaya. Yang penting adalah mendefinisikan batas stale yang dapat diterima. Jika output dipakai untuk keputusan sensitif, pertimbangkan TTL pendek, versioned cache key, atau invalidasi berbasis event.

Mencegah cache stampede

Stampede terjadi ketika banyak request mengakses item yang sama tepat saat cache kosong atau expired. Tanpa perlindungan, semua request akan memicu pekerjaan mahal yang sama.

Pencegahannya:

  • single-flight dengan lock per cache key,
  • request coalescing, yaitu banyak requester menunggu hasil job yang sama,
  • jitter pada TTL agar item populer tidak expired bersamaan,
  • stale-while-revalidate untuk item yang boleh sedikit usang.

Distributed lock: mencegah duplicate work

Distributed lock dipakai saat ada banyak instance worker atau API service. Tujuannya bukan membuat sistem “selalu benar”, tetapi mengurangi peluang dua proses mengerjakan pekerjaan identik secara bersamaan.

Kapan lock dibutuhkan

  • Satu job bisa terbentuk lebih dari sekali akibat retry dari klien.
  • Queue bisa memunculkan redelivery.
  • Beberapa worker memproses pekerjaan yang kuncinya sama.
  • Cache miss untuk item populer terjadi bersamaan.

Prinsip implementasi lock

  • Gunakan key yang stabil, biasanya turunan dari idempotency key atau cache key.
  • Berikan TTL pada lock agar lock tidak menggantung selamanya saat worker crash.
  • Simpan owner token dan hanya owner yang boleh melepas lock.
  • Jangan anggap lock sebagai pengganti idempotency. Keduanya saling melengkapi.

Kesalahan umum adalah memakai lock tanpa TTL, atau melepas lock tanpa memverifikasi owner token. Itu bisa menimbulkan balapan antar worker.

Retry, backoff, dan DLQ

Retry hanya untuk error sementara

Tidak semua kegagalan pantas di-retry. Retry cocok untuk:

  • timeout upstream,
  • error jaringan sementara,
  • respons vendor yang menandakan beban sementara atau rate limit.

Sebaliknya, request invalid, parameter salah, atau otorisasi gagal biasanya termasuk gagal permanen dan tidak sebaiknya diulang tanpa perubahan input.

Backoff dan jitter

Gunakan backoff bertahap agar retry tidak memperburuk lonjakan. Tambahkan jitter agar banyak worker tidak menembak ulang vendor pada waktu yang sama. Ini penting saat vendor mengaktifkan rate limit atau saat upstream baru pulih.

DLQ

Dead-letter queue berguna untuk menahan job bermasalah agar tidak berputar tanpa henti di jalur utama. DLQ membantu dua hal:

  • menjaga queue utama tetap sehat,
  • memberi tempat investigasi untuk pola error tertentu.

Saat job masuk DLQ, simpan alasan lengkap, jumlah retry, payload yang relevan, dan korelasi ke request awal. Buat prosedur yang jelas: apakah job akan diputar ulang manual, diperbaiki payload-nya, atau ditandai gagal permanen.

Strategi menjaga consistency

Pada sistem async, konsistensi biasanya eventual, bukan instan. Tantangannya adalah mencegah status dan hasil saling bertentangan.

Urutan tulis yang aman

Pola yang lazim:

  1. Worker menyelesaikan pemanggilan model.
  2. Simpan hasil final ke penyimpanan utama.
  3. Update status job menjadi completed.
  4. Isi cache untuk pembacaan cepat.

Urutan ini membantu mengurangi kasus cache berisi hasil tetapi database belum menandai job selesai. Jika perlu, bungkus update yang berhubungan dalam transaksi sesuai kemampuan storage yang dipakai.

Outbox atau event setelah commit

Jika sistem Anda mengirim event saat job selesai, kirim event hanya setelah state final benar-benar tersimpan. Pola outbox dapat membantu mencegah event terkirim tetapi data utama gagal commit.

Idempotent completion

Pastikan proses penyelesaian job sendiri idempotent. Jika worker memproses ulang akibat redelivery setelah sukses parsial, operasi “tulis hasil” dan “ubah status” sebaiknya aman dipanggil lagi tanpa membuat duplikasi atau overwrite yang tidak diinginkan.

Pseudocode generik

Berikut contoh alur generik yang tidak bergantung pada framework tertentu.

// API handler
function submitInference(request):
    normalizedInput = normalize(request.input)
    jobKey = hash(request.tenantId, request.model, normalizedInput, request.options)
    cacheKey = "ai-result:" + jobKey

    cached = cache.get(cacheKey)
    if cached is not null and not cached.isExpired():
        return { status: "completed", source: "cache", result: cached.value }

    existingJob = jobs.findByIdempotencyKey(jobKey)
    if existingJob exists:
        return { status: existingJob.status, jobId: existingJob.id }

    job = jobs.insertIfAbsent({
        idempotencyKey: jobKey,
        status: "queued",
        payload: request.payload,
        attempts: 0
    })

    if job.created:
        queue.publish({ jobId: job.id })

    return { status: "queued", jobId: job.id }
// Worker
function processJob(message):
    job = jobs.get(message.jobId)
    if job is null:
        ack(message)
        return

    if job.status == "completed":
        ack(message)
        return

    lockKey = "lock:job:" + job.idempotencyKey
    lock = lockService.acquire(lockKey, ttl=60s)
    if not lock.acquired:
        requeueWithDelay(message)
        return

    try:
        jobs.markProcessing(job.id)

        cached = cache.get("ai-result:" + job.idempotencyKey)
        if cached is not null and not cached.isExpired():
            jobs.storeResultAndComplete(job.id, cached.value, source="cache")
            ack(message)
            return

        response = vendor.callModel(job.payload, timeout=upstreamTimeout)

        jobs.storeResult(job.id, response)
        jobs.markCompleted(job.id)
        cache.set(
            "ai-result:" + job.idempotencyKey,
            response,
            ttl=resultTtlWithJitter()
        )
        ack(message)

    catch error:
        if isRetryable(error) and job.attempts < MAX_ATTEMPTS:
            jobs.markRetrying(job.id, error)
            retryLater(message, backoffWithJitter(job.attempts))
        else:
            jobs.markFailed(job.id, error)
            dlq.publish({ jobId: job.id, reason: error.code })
            ack(message)
    finally:
        lock.releaseIfOwner(lock)

Masalah operasional nyata dan cara menanganinya

1. Job dobel walau sudah ada queue

Penyebab umum:

  • klien mengirim ulang request,
  • broker melakukan redelivery,
  • worker crash setelah upstream sukses tetapi sebelum ack.

Penanganan:

  • idempotency key yang konsisten,
  • insert job secara atomik jika belum ada,
  • completion logic yang idempotent,
  • lock untuk mencegah proses paralel atas key yang sama.

2. Timeout ke upstream vendor

Jangan gunakan timeout default yang tidak jelas. Pisahkan:

  • connect timeout untuk koneksi awal,
  • read/response timeout untuk menunggu hasil,
  • job lease/visibility timeout agar worker lain tidak mengambil job terlalu cepat.

Jika timeout worker lebih pendek daripada latensi aktual vendor, sistem akan terlihat “gagal” padahal upstream masih bekerja.

3. Rate limit vendor

Queue saja tidak otomatis menyelesaikan rate limit. Anda tetap perlu kontrol concurrency dan throughput di sisi worker. Praktiknya bisa berupa:

  • membatasi jumlah worker aktif untuk model tertentu,
  • token bucket atau semaphore per vendor/model,
  • queue terpisah per prioritas atau tenant.

Jika semua tenant berbagi jalur yang sama, satu tenant yang spike bisa memonopoli kapasitas. Pertimbangkan fairness atau kuota per tenant.

4. Cache stale yang membingungkan pengguna

Masalah ini sering muncul saat hasil AI dipakai sebagai jawaban final. Dokumentasikan apakah endpoint mengembalikan hasil fresh atau hasil cache. Jika perlu, sediakan mode bypass cache untuk operasi tertentu, tetapi lindungi agar tidak merusak budget dan rate limit.

5. Stampede setelah TTL habis

Ini sering terlihat pada prompt populer. Solusinya bukan hanya memperpanjang TTL, tetapi memakai refresh bertahap, soft TTL, dan lock per key saat regenerasi hasil.

Observabilitas yang wajib ada

Tanpa observabilitas, masalah pada sistem async sering terlihat sebagai “lambat” atau “kadang gagal” tanpa penyebab jelas.

Metrics

  • panjang queue,
  • waktu tunggu job di queue,
  • latency pemrosesan worker,
  • cache hit ratio,
  • jumlah lock contention,
  • retry rate,
  • DLQ rate,
  • error rate per vendor/model/tenant.

Log terstruktur

Sertakan field seperti:

  • request ID,
  • job ID,
  • idempotency key,
  • tenant ID,
  • model name,
  • attempt number,
  • error class,
  • upstream latency.

Tracing

Jika memungkinkan, hubungkan trace dari API → queue publish → worker → vendor call → penyimpanan hasil. Ini sangat membantu saat mencari bottleneck apakah ada di aplikasi sendiri, broker, cache, atau vendor.

Trade-off desain

Queue async vs request sinkron

  • Async: lebih tahan lonjakan, lebih cocok untuk request mahal, tetapi lebih kompleks dan hasil tidak langsung tersedia.
  • Sinkron: lebih sederhana untuk UX tertentu, tetapi rawan timeout dan sulit menghadapi rate limit serta burst.

Cache agresif vs freshness tinggi

  • Cache agresif: hemat biaya dan cepat, tetapi risiko stale lebih besar.
  • Freshness tinggi: hasil lebih baru, tetapi biaya dan latency naik.

Lock ketat vs throughput

  • Lock ketat: duplicate work turun, tetapi contention bisa naik.
  • Lock longgar: throughput lebih bebas, tetapi ada risiko duplikasi saat burst.

Checklist implementasi

  • Tentukan apakah endpoint harus sinkron, async, atau hybrid.
  • Definisikan idempotency key dari input yang telah dinormalisasi.
  • Gunakan penyimpanan job dengan status yang eksplisit.
  • Pastikan enqueue aman terhadap request duplikat.
  • Tambah distributed lock dengan TTL dan owner token.
  • Tentukan kebijakan cache: key, TTL, soft/hard expiry, dan invalidasi.
  • Batasi concurrency worker per vendor atau per model.
  • Terapkan retry hanya untuk error yang memang retryable.
  • Gunakan backoff dengan jitter.
  • Sediakan DLQ dan prosedur operasional untuk menanganinya.
  • Pastikan completion logic idempotent.
  • Pasang metrics, log terstruktur, dan tracing sejak awal.
  • Uji skenario burst, timeout, redelivery, dan cache stampede.

Kesalahan yang sering terjadi

  • Menganggap queue otomatis menghilangkan duplicate work tanpa idempotency.
  • Memakai cache key yang berbeda-beda untuk input yang sebenarnya sama.
  • Lock tanpa TTL sehingga pekerjaan macet saat worker crash.
  • Retry semua jenis error, termasuk error validasi.
  • Tidak membedakan timeout koneksi, timeout respons, dan visibility timeout.
  • Tidak menyimpan metadata error yang cukup untuk investigasi.
  • Tidak membatasi concurrency sehingga tetap menabrak rate limit vendor.

Penutup

Untuk akses model AI yang mahal, dibatasi vendor, dan tidak selalu cocok dipanggil secara sinkron, kombinasi worker queue, cache, distributed lock, dan idempotency adalah fondasi yang kuat. Queue meratakan lonjakan, worker memindahkan pekerjaan ke jalur async, cache mengurangi request mahal, dan lock mencegah banyak proses mengerjakan hal yang sama.

Yang membedakan desain yang sekadar jalan dan yang siap produksi adalah detail operasionalnya: TTL yang tepat, pencegahan stampede, retry yang disiplin, DLQ yang terurus, serta observabilitas yang cukup untuk memahami perilaku sistem saat vendor lambat atau traffic mendadak naik.