Pertanyaan utama saat aplikasi Go Fiber mulai ramai bukan sekadar "bisa lebih cepat atau tidak", tetapi apakah suatu pekerjaan masih pantas dikerjakan di jalur request-response sinkron atau sebaiknya dipindahkan ke worker asinkron. Jawabannya bergantung pada durasi kerja, risiko timeout, pola lonjakan trafik, kebutuhan konsistensi, dan seberapa besar toleransi sistem terhadap keterlambatan hasil.

Secara praktis, API sinkron masih tepat jika pekerjaan ringan, hasil harus langsung tersedia, dan dependensi eksternal relatif stabil. Sebaliknya, worker asinkron mulai layak ketika request memicu proses berat, rentan timeout, sering terkena lonjakan beban, atau perlu isolasi kegagalan agar web server tidak ikut terseret masalah.

Memahami perbedaan API sinkron dan worker asinkron

Pada model sinkron, client mengirim request ke API Go Fiber, server memproses seluruh pekerjaan, lalu mengembalikan respons ketika pekerjaan selesai. Selama itu, koneksi request masih aktif dan resource server ikut tertahan.

Pada model asinkron, API hanya menerima permintaan, memvalidasi data, membuat entri job, lalu mengirimkannya ke queue. Respons dikembalikan lebih cepat, biasanya dengan status bahwa pekerjaan sedang diproses. Eksekusi sebenarnya dilakukan oleh worker terpisah.

Contoh alur sinkron

  • Client memanggil POST /reports
  • API memproses query, generate file, simpan hasil
  • API mengembalikan file atau URL hasil saat itu juga

Contoh alur asinkron

  • Client memanggil POST /reports
  • API membuat job dan mengembalikan 202 Accepted dengan job_id
  • Worker mengambil job dari queue dan menjalankan proses generate
  • Client mengecek status lewat GET /jobs/:id atau menerima callback/webhook

Perbedaan ini terlihat sederhana, tetapi implikasinya besar terhadap latency, throughput, reliabilitas, dan operasional harian.

Trade-off utama: latency, throughput, dan isolasi kegagalan

1. Latency

Jika semua dikerjakan sinkron, client bisa langsung menerima hasil final. Ini ideal untuk operasi cepat seperti validasi, baca data, atau update ringan. Masalah muncul ketika request mulai memanggil layanan eksternal lambat, menjalankan query berat, melakukan upload besar, mengirim email, atau membuat laporan besar. Latency naik, dan timeout mulai sering terjadi.

Dengan worker asinkron, latency yang dirasakan client untuk submit request turun drastis karena API hanya mencatat job. Namun, latency end-to-end sampai hasil final tersedia bisa lebih lama. Jadi, asinkron bukan berarti semuanya lebih cepat; yang berubah adalah jalur kritis request.

2. Throughput

Pada jalur sinkron, request lama akan mengikat goroutine, koneksi database, socket ke layanan lain, dan resource CPU atau memori lebih lama. Saat traffic naik, throughput menurun karena kapasitas server habis untuk pekerjaan panjang.

Pada arsitektur worker, API tetap ringan dan bisa melayani lebih banyak request masuk. Beban berat dipindahkan ke worker yang jumlahnya dapat diatur terpisah. Ini membantu ketika pola beban tidak rata atau ada pekerjaan yang jauh lebih mahal daripada request biasa.

3. Isolasi kegagalan

Jika satu integrasi eksternal lambat atau bermasalah, endpoint sinkron akan ikut gagal atau timeout. Dalam model worker, Anda bisa mengisolasi kegagalan itu di proses belakang layar. API publik tetap responsif, sementara worker bisa retry, backoff, atau memindahkan job gagal ke dead-letter handling.

Aturan praktis: jika kegagalan proses berat tidak seharusnya merusak pengalaman dasar API utama, pertimbangkan worker asinkron.

Kapan pendekatan sinkron masih cukup?

Tidak semua sistem perlu queue sejak awal. Pendekatan sinkron masih baik jika kondisi berikut terpenuhi:

  • Pekerjaan cepat dan terukur, misalnya validasi, CRUD sederhana, atau query ringan.
  • Client memang butuh hasil langsung, misalnya autentikasi, kalkulasi harga real-time, atau pengecekan stok cepat.
  • Timeout jarang terjadi dan latensi masih stabil di bawah batas SLA internal Anda.
  • Dependensi eksternal minim atau relatif andal.
  • Tim masih kecil dan belum ingin menambah kompleksitas queue, retry, monitoring worker, serta status job.
  • Konsistensi langsung dibutuhkan, misalnya setelah update selesai, client harus langsung melihat state final tanpa status perantara.

Pada tahap ini, optimasi yang lebih murah sering kali cukup: perbaiki query, tambahkan caching, gunakan timeout yang sehat, batasi concurrency ke layanan eksternal, atau pecah endpoint yang terlalu gemuk.

Kapan worker asinkron mulai layak di Go Fiber?

Biasanya keputusan pindah ke worker tidak dipicu satu faktor saja, melainkan kombinasi beberapa sinyal teknis.

Sinyal yang umum muncul

  • Timeout mulai sering pada endpoint tertentu.
  • Lonjakan beban membuat request lambat meskipun operasi bisnis tidak berubah.
  • Pekerjaan berat seperti generate laporan, impor data, kompres file, resize media, sinkronisasi ke pihak ketiga, atau pengiriman notifikasi massal.
  • Kebutuhan retry karena integrasi eksternal kadang gagal sementara.
  • Kebutuhan isolasi kegagalan agar error proses belakang tidak langsung menjatuhkan API utama.
  • Perlu pembatasan laju proses ke sistem lain tanpa menolak semua request dari client.
  • Resource contention, misalnya worker CPU-heavy mengganggu request baca/tulis biasa jika diproses di jalur sinkron.

Jika Anda melihat pola seperti endpoint tertentu menyumbang timeout terbanyak, antrean koneksi ke database meningkat saat job berat berjalan, atau pod web harus di-scale hanya untuk menahan lonjakan proses batch, itu tanda kuat bahwa beban sebaiknya dipisahkan.

Trade-off yang sering terlupakan

Retry bukan sekadar menjalankan ulang

Pada worker, retry adalah fitur penting, tetapi juga sumber bug jika tidak dirancang dengan benar. Masalah utamanya adalah duplikasi efek samping: email terkirim dua kali, tagihan tercatat dua kali, atau data pihak ketiga dibuat ulang.

Karena itu, sistem asinkron hampir selalu butuh idempotensi. Artinya, menjalankan job yang sama beberapa kali tetap menghasilkan state akhir yang aman.

Idempotensi harus eksplisit

Beberapa cara praktis:

  • Simpan idempotency key dari client atau hasil hashing payload penting.
  • Beri unique constraint pada tabel yang mewakili efek samping tertentu.
  • Simpan status job: pending, processing, succeeded, failed.
  • Sebelum eksekusi, cek apakah efek akhir sudah pernah berhasil dibuat.

Tanpa idempotensi, retry akan memperbaiki satu masalah tetapi menciptakan masalah lain yang lebih sulit dilacak.

Observabilitas jadi lebih penting

Pada sistem sinkron, Anda bisa melihat error langsung di log request. Pada sistem asinkron, alur terpecah: request masuk, job dibuat, job diproses, mungkin retry, lalu hasil disimpan. Tanpa observabilitas yang memadai, debugging akan terasa seperti mengejar jejak yang terputus.

Minimal, siapkan:

  • Correlation ID atau request/job ID yang konsisten.
  • Log terstruktur untuk API dan worker.
  • Metrik jumlah job: pending, success, failed, retry.
  • Durasi antrean dan durasi eksekusi worker.
  • Error rate per jenis job dan per dependensi eksternal.

Konsistensi data tidak lagi sesederhana commit lalu selesai

Pada model sinkron, Anda cenderung berpikir dalam satu alur transaksi: request datang, database diubah, respons dikembalikan. Pada model asinkron, ada kemungkinan state perantara: data utama sudah tersimpan, tetapi proses turunannya belum selesai.

Karena itu, Anda perlu mendesain konsistensi yang realistis:

  • Apakah sistem menerima eventual consistency?
  • Bagian mana yang harus langsung konsisten?
  • Bagaimana jika job gagal permanen setelah data awal terlanjur tersimpan?

Sering kali solusi terbaik adalah memisahkan aksi inti dan efek samping. Contohnya, order dibuat secara sinkron, tetapi email invoice dan sinkronisasi ke sistem analitik diproses asinkron.

Biaya infrastruktur dan operasional bertambah

Queue dan worker bukan gratis. Anda perlu menambah komponen seperti Redis, message broker, atau minimal tabel jobs di database. Lalu ada biaya monitoring, alarm, dashboard, pembersihan job lama, dan prosedur operasional saat antrean macet.

Kalau volume kerja masih kecil dan endpoint masih sehat, menambahkan worker terlalu cepat bisa membuat sistem lebih rumit daripada manfaatnya.

Maintainability tim juga faktor arsitektur

Desain terbaik di atas kertas belum tentu terbaik untuk tim Anda. Sistem asinkron menuntut disiplin lebih tinggi: kontrak payload job, retry policy, status job, replay, observabilitas, dan prosedur recovery. Jika tim belum siap menjaga semua ini, pendekatan sinkron yang sederhana sering lebih sehat.

Pola implementasi realistis di Go Fiber

Berikut pola dasar yang sering dipakai: API Go Fiber menerima request, menyimpan data inti, membuat job, lalu worker memproses job tersebut.

Pola endpoint yang umum

  • POST /exports untuk submit pekerjaan
  • GET /jobs/:id untuk cek status
  • GET /exports/:id untuk ambil hasil jika sudah jadi

Untuk operasi asinkron, respons 202 Accepted umumnya lebih tepat daripada 200 OK jika pekerjaan belum selesai.

Contoh sederhana handler Go Fiber

package main

import (
  "context"
  "encoding/json"
  "log"
  "time"

  "github.com/gofiber/fiber/v2"
)

type CreateExportRequest struct {
  UserID string `json:"user_id"`
  Format string `json:"format"`
}

type JobPayload struct {
  JobID  string `json:"job_id"`
  UserID string `json:"user_id"`
  Format string `json:"format"`
}

func createExport(c *fiber.Ctx) error {
  var req CreateExportRequest
  if err := c.BodyParser(&req); err != nil {
    return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "payload tidak valid"})
  }

  if req.UserID == "" || req.Format == "" {
    return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "user_id dan format wajib diisi"})
  }

  jobID := generateJobID()

  // 1. Simpan metadata job ke database: pending
  if err := saveJob(jobID, "pending", req.UserID, req.Format); err != nil {
    return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "gagal menyimpan job"})
  }

  // 2. Publish job ke queue
  payload := JobPayload{JobID: jobID, UserID: req.UserID, Format: req.Format}
  raw, _ := json.Marshal(payload)
  if err := enqueue(context.Background(), raw); err != nil {
    // Idealnya update status job agar terlihat gagal dikirim ke queue
    markJobFailed(jobID, "enqueue gagal")
    return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "gagal mengantrikan job"})
  }

  return c.Status(fiber.StatusAccepted).JSON(fiber.Map{
    "job_id": jobID,
    "status": "pending",
  })
}

func main() {
  app := fiber.New()
  app.Post("/exports", createExport)
  log.Fatal(app.Listen(":3000"))
}

func generateJobID() string { return time.Now().Format("20060102150405.000000000") }
func saveJob(jobID, status, userID, format string) error { return nil }
func markJobFailed(jobID, reason string) error { return nil }
func enqueue(ctx context.Context, payload []byte) error { return nil }

Contoh di atas sengaja sederhana. Dalam implementasi nyata, pembuatan job dan publikasi ke queue perlu didesain agar tidak menimbulkan inkonsistensi jika salah satunya berhasil dan yang lain gagal.

Pola queue sederhana

Untuk tahap awal, ada dua pendekatan umum:

  1. Queue berbasis database: tabel jobs/pending_tasks. Mudah diadopsi, tetapi perlu hati-hati terhadap locking, polling, dan throughput.
  2. Queue berbasis broker atau Redis: lebih cocok jika volume meningkat dan Anda butuh worker terpisah yang mudah di-scale.

Jika kebutuhan belum besar, tabel jobs bisa cukup. Namun pastikan worker mengambil job secara aman agar tidak diproses ganda tanpa kontrol.

Contoh loop worker sederhana

package main

import (
  "context"
  "encoding/json"
  "log"
  "time"
)

type JobPayload struct {
  JobID  string `json:"job_id"`
  UserID string `json:"user_id"`
  Format string `json:"format"`
}

func workerLoop(ctx context.Context) {
  for {
    select {
    case <-ctx.Done():
      return
    default:
      raw, err := dequeue(ctx)
      if err != nil {
        time.Sleep(1 * time.Second)
        continue
      }

      var job JobPayload
      if err := json.Unmarshal(raw, &job); err != nil {
        log.Println("payload job tidak valid:", err)
        continue
      }

      if err := markJobProcessing(job.JobID); err != nil {
        log.Println("gagal update status processing:", err)
        continue
      }

      err = processExport(ctx, job)
      if err != nil {
        log.Println("job gagal:", job.JobID, err)
        scheduleRetryOrFail(job.JobID, err)
        continue
      }

      if err := markJobSucceeded(job.JobID); err != nil {
        log.Println("gagal update status success:", err)
      }
    }
  }
}

func dequeue(ctx context.Context) ([]byte, error) { return nil, context.DeadlineExceeded }
func markJobProcessing(jobID string) error { return nil }
func processExport(ctx context.Context, job JobPayload) error { return nil }
func scheduleRetryOrFail(jobID string, err error) {}
func markJobSucceeded(jobID string) error { return nil }

Poin pentingnya bukan bentuk loop-nya, tetapi kontrol status job, retry, dan pencatatan error.

Hal penting dalam desain retry dan idempotensi

Bedakan error sementara dan error permanen

Retry cocok untuk error sementara, misalnya koneksi ke layanan eksternal putus sesaat atau respons timeout. Retry tidak cocok untuk payload salah, data referensi tidak ditemukan, atau validasi bisnis gagal.

Kalau semua error diretry tanpa klasifikasi, antrean akan penuh oleh job yang tidak mungkin sukses.

Gunakan backoff yang masuk akal

Jangan langsung mengulang puluhan kali tanpa jeda. Beri jarak antar percobaan agar tidak memperburuk sistem yang sedang bermasalah. Selain itu, tetapkan batas percobaan maksimal dan tandai job sebagai gagal permanen bila sudah melewati ambang tersebut.

Simpan jejak retry

Minimal simpan jumlah percobaan, waktu terakhir gagal, dan pesan error ringkas. Data ini sangat membantu saat insiden terjadi.

Sinkron dan asinkron sering dipakai bersamaan

Pilihan arsitektur terbaik sering bukan hitam-putih. Banyak sistem sehat menggunakan kombinasi:

  • Sinkron untuk aksi inti yang harus langsung berhasil atau gagal.
  • Asinkron untuk efek samping, proses berat, atau integrasi yang lambat.

Contoh yang umum:

  • Menyimpan order secara sinkron.
  • Mengirim email konfirmasi secara asinkron.
  • Menghasilkan PDF invoice secara asinkron.
  • Mengirim event analitik dan sinkronisasi ke pihak ketiga secara asinkron.

Pendekatan ini menekan kompleksitas sambil tetap menjaga performa dan pengalaman pengguna.

Common mistakes saat migrasi ke worker

  • Memindahkan semua hal ke queue meskipun sebenarnya cepat dan perlu hasil langsung.
  • Tidak menyediakan endpoint status job, sehingga client tidak tahu hasil akhir.
  • Tidak memikirkan idempotensi, lalu efek samping menjadi duplikat saat retry.
  • Tidak membedakan error retryable dan non-retryable.
  • Tidak menyiapkan monitoring antrean, sehingga job menumpuk tanpa diketahui.
  • Menggabungkan terlalu banyak tanggung jawab dalam satu jenis job, membuat debugging dan recovery sulit.
  • Mengabaikan kapasitas downstream, misalnya worker terlalu agresif memukul database atau API pihak ketiga.

Tips debugging dan observabilitas

  • Masukkan job_id ke semua log yang terkait.
  • Teruskan request ID dari API ke payload job jika relevan.
  • Pantau metrik dasar: panjang antrean, umur job tertua, durasi proses, retry rate, failure rate.
  • Buat dashboard terpisah untuk web API dan worker agar bottleneck mudah dibedakan.
  • Jika job menghasilkan artefak seperti file, simpan referensi hasil dan status akhirnya dengan jelas.

Kalau Anda tidak bisa menjawab pertanyaan "job mana yang gagal, sudah dicoba berapa kali, dan sedang menunggu apa", berarti observabilitas sistem asinkron Anda belum cukup.

Checklist keputusan arsitektur yang realistis

Gunakan daftar ini sebelum memutuskan tetap sinkron atau mulai menambah worker asinkron di Go Fiber.

Tetap sinkron jika:

  • Durasi kerja singkat dan relatif konsisten.
  • Client perlu hasil final dalam respons yang sama.
  • Beban puncak masih bisa ditangani dengan optimasi biasa.
  • Kegagalan proses harus langsung terlihat ke client.
  • Tim ingin menjaga sistem tetap sederhana.

Pertimbangkan worker asinkron jika:

  • Endpoint tertentu sering timeout atau latensinya sangat bervariasi.
  • Pekerjaan berat mengganggu request lain.
  • Ada lonjakan trafik yang membuat jalur request utama ikut melambat.
  • Anda butuh retry aman untuk error sementara.
  • Perlu isolasi kegagalan dari integrasi eksternal.
  • Hasil tidak harus langsung tersedia saat request selesai.
  • Tim siap mengelola status job, queue, monitoring, dan recovery.

Pertanyaan kunci sebelum implementasi

  1. Apa pekerjaan ini benar-benar harus selesai sebelum respons dikirim?
  2. Apa risiko jika pekerjaan gagal beberapa menit setelah request diterima?
  3. Apakah proses ini aman dijalankan ulang?
  4. Bagaimana client mengetahui status dan hasil akhir?
  5. Bagaimana jika queue penuh atau worker berhenti?
  6. Bagaimana cara mendeteksi job macet?
  7. Apakah eventual consistency dapat diterima oleh bisnis?

Kesimpulan

Dalam aplikasi Go Fiber, API sinkron vs worker asinkron bukan soal mana yang lebih modern, tetapi mana yang paling tepat untuk karakter beban kerja Anda. Jika operasi ringan, hasil harus langsung tersedia, dan sistem masih stabil, pendekatan sinkron biasanya cukup dan lebih mudah dirawat.

Worker asinkron mulai layak saat pekerjaan berat, timeout meningkat, trafik melonjak, atau integrasi eksternal membuat request utama rapuh. Keuntungannya adalah latency request lebih rendah, throughput lebih baik, dan isolasi kegagalan yang lebih sehat. Namun, Anda membayarnya dengan kompleksitas tambahan: retry, idempotensi, status job, observabilitas, dan operasional queue.

Pendekatan yang paling realistis biasanya bertahap: tetap sinkron untuk alur inti, pindahkan efek samping dan proses berat ke worker. Dengan begitu, Anda mendapat manfaat skalabilitas tanpa langsung membuat arsitektur jauh lebih rumit dari yang dibutuhkan.