Pelajari pola antrean pekerjaan latar belakang sederhana untuk mengirim email, menjalankan laporan, dan mengirim webhook dengan retry, backoff, dan penanganan dead-letter, tanpa alat berat.

Pekerjaan yang bisa memakan waktu lebih dari satu atau dua detik sebaiknya tidak berjalan di dalam permintaan pengguna. Mengirim email, membuat laporan, dan mengirim webhook bergantung pada jaringan, layanan pihak ketiga, atau query lambat. Kadang mereka berhenti sebentar, gagal, atau membutuhkan waktu lebih lama dari yang Anda duga.
Jika Anda melakukan pekerjaan itu sementara pengguna menunggu, orang akan langsung menyadarinya. Halaman macet, tombol "Simpan" berputar, dan permintaan timeout. Retry juga bisa terjadi di tempat yang salah. Pengguna merefresh, load balancer Anda melakukan retry, atau frontend mengirim ulang, dan Anda berakhir dengan email ganda, panggilan webhook ganda, atau dua proses laporan yang saling bersaing.
Pekerjaan latar belakang memperbaiki ini dengan menjaga permintaan kecil dan dapat diprediksi: terima aksi, catat job untuk dikerjakan nanti, balas cepat. Job berjalan di luar permintaan, dengan aturan yang Anda kendalikan.
Bagian yang sulit adalah keandalan. Setelah pekerjaan keluar dari jalur permintaan, Anda tetap harus menjawab pertanyaan seperti:
Banyak tim merespons dengan menambahkan "infrastruktur berat": message broker, fleet worker terpisah, dashboard, alerting, dan playbook. Alat-alat itu berguna saat memang diperlukan, tapi mereka juga menambah bagian yang bergerak dan mode kegagalan baru.
Tujuan awal yang lebih baik lebih sederhana: job yang andal menggunakan bagian yang sudah Anda miliki. Untuk kebanyakan produk, itu berarti antrean berbasis database plus proses worker kecil. Tambahkan strategi retry dan backoff yang jelas, serta pola dead-letter untuk job yang terus gagal. Anda mendapatkan perilaku yang dapat diprediksi tanpa berkomitmen pada platform kompleks sejak hari pertama.
Bahkan jika Anda membangun cepat dengan alat chat-driven seperti Koder.ai, pemisahan ini tetap penting. Pengguna harus mendapat respons cepat sekarang, dan sistem Anda harus menyelesaikan pekerjaan yang lambat dan rawan gagal dengan aman di latar belakang.
Antrean adalah barisan menunggu untuk pekerjaan. Alih-alih melakukan tugas lambat atau tidak dapat diandalkan selama permintaan pengguna (mengirim email, membuat laporan, memanggil webhook), Anda menaruh catatan kecil ke antrean dan mengembalikan respons cepat. Nanti, proses terpisah mengambil catatan itu dan melakukan pekerjaannya.
Beberapa istilah yang sering Anda temui:
Alur paling sederhana terlihat seperti ini:
Enqueue: aplikasi Anda menyimpan record job (tipe, payload, waktu jalan).
Claim: worker menemukan job yang tersedia berikutnya dan "mengunci"nya agar hanya satu worker yang menjalankannya.
Run: worker melakukan tugas (mengirim, membuat, mengantarkan).
Finish: tandai selesai, atau catat kegagalan dan atur waktu jalan berikutnya.
Jika volume job Anda sederhana dan Anda sudah punya database, antrean berbasis database seringkali sudah cukup. Mudah dipahami, mudah di-debug, dan cocok untuk kebutuhan umum seperti pemrosesan job email dan keandalan pengiriman webhook.
Platform streaming mulai masuk akal ketika Anda membutuhkan throughput sangat tinggi, banyak konsumen independen, atau kemampuan untuk replay riwayat event besar ke banyak sistem. Jika Anda menjalankan puluhan layanan dengan jutaan event per jam, alat seperti Kafka bisa membantu. Sampai saat itu, tabel database plus loop worker mencakup banyak antrean dunia nyata.
Antrean berbasis database tetap tertib jika setiap record job menjawab tiga pertanyaan dengan cepat: apa yang harus dilakukan, kapan mencoba lagi, dan apa yang terjadi terakhir kali. Jika itu benar, operasi menjadi membosankan (itu tujuannya).
Simpan input terkecil yang diperlukan untuk melakukan pekerjaan, bukan seluruh output yang sudah dirender. Payload yang baik adalah ID dan beberapa parameter, seperti { "user_id": 42, "template": "welcome" }.
Hindari menyimpan blob besar (HTML email penuh, data laporan besar, body webhook raksasa). Ini membuat database Anda tumbuh lebih cepat dan mempersulit debugging. Jika job perlu dokumen besar, simpan referensi saja: report_id, export_id, atau key file. Worker bisa mengambil data lengkap saat dijalankan.
Minimalnya, sediakan ruang untuk:
job_type memilih handler (send_email, generate_report, deliver_webhook). payload menyimpan input kecil seperti ID dan opsi.queued, running, succeeded, failed, dead).attempt_count dan max_attempts agar Anda bisa berhenti saat jelas tidak akan berhasil.created_at dan next_run_at (kapan job menjadi eligible). Tambahkan started_at dan finished_at jika ingin visibilitas lebih baik untuk job lambat.idempotency_key untuk mencegah efek ganda, dan last_error agar Anda bisa melihat kenapa gagal tanpa menggali tumpukan log.Idempotensi terdengar mewah, tapi idenya sederhana: jika job yang sama berjalan dua kali, percobaan kedua harus mendeteksi itu dan tidak melakukan hal berbahaya. Contohnya, job pengiriman webhook bisa menggunakan idempotency key seperti webhook:order:123:event:paid sehingga Anda tidak mengirim event yang sama dua kali jika retry tumpang tindih dengan timeout.
Juga tangkap beberapa angka dasar sejak awal. Anda tidak perlu dashboard besar untuk memulai, cukup query yang memberi tahu: berapa banyak job yang antre, berapa banyak yang gagal, dan umur job antrean tertua.
Jika Anda sudah punya database, Anda bisa memulai antrean latar belakang tanpa menambah infrastruktur baru. Job adalah baris, dan worker adalah proses yang terus mengambil baris yang sudah waktunya dan melakukan pekerjaan.
Jaga tabel tetap kecil dan sederhana. Anda ingin field yang cukup untuk menjalankan, melakukan retry, dan melakukan debugging later.
CREATE TABLE jobs (
id bigserial PRIMARY KEY,
job_type text NOT NULL,
payload jsonb NOT NULL,
status text NOT NULL DEFAULT 'queued', -- queued, running, done, failed
attempts int NOT NULL DEFAULT 0,
next_run_at timestamptz NOT NULL DEFAULT now(),
locked_at timestamptz,
locked_by text,
last_error text,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX jobs_due_idx ON jobs (status, next_run_at);
Jika Anda membangun di Postgres (umum pada backend Go), jsonb praktis untuk menyimpan data job seperti { "user_id":123,"template":"welcome" }.
Saat aksi pengguna harus memicu job (mengirim email, memicu webhook), tulis baris job dalam transaksi database yang sama dengan perubahan utama jika memungkinkan. Itu mencegah "pengguna dibuat tapi job hilang" jika crash terjadi tepat setelah penulisan utama.
Contoh: saat user mendaftar, insert row user dan job send_welcome_email dalam satu transaksi.
Worker mengulang siklus yang sama: temukan satu job yang sudah waktunya, klaim agar tidak ada yang lain mengambilnya, proses, lalu tandai selesai atau jadwalkan retry.
Dalam praktiknya, itu berarti:
status='queued' dan next_run_at <= now().SELECT ... FOR UPDATE SKIP LOCKED adalah pendekatan umum).status='running', locked_at=now(), locked_by='worker-1'.done/succeeded), atau catat last_error dan jadwalkan percobaan berikutnya.Beberapa worker bisa berjalan bersamaan. Langkah klaim yang atomik mencegah pengambilan ganda.
Saat shutdown, berhenti mengambil job baru, selesaikan job yang sedang berjalan, lalu keluar. Jika proses mati di tengah job, gunakan aturan sederhana: anggap job yang bertahan di running melewati timeout sebagai eligible untuk dikeluarkan lagi oleh tugas periodik "reaper".
Jika Anda membangun di Koder.ai, pola antrean-database ini adalah default yang solid untuk email, laporan, dan webhook sebelum Anda menambahkan layanan antrean khusus.
Retry menjaga antrean tetap tenang saat dunia nyata berantakan. Tanpa aturan jelas, retry berubah jadi loop berisik yang menyebalkan pengguna, membombardir API, dan menyembunyikan bug nyata.
Mulailah dengan memutuskan apa yang harus diretry dan apa yang harus gagal cepat.
Retry masalah sementara: timeout jaringan, 502/503, rate limit, atau gangguan koneksi database singkat.
Gagal cepat saat job tidak akan berhasil: alamat email hilang, response 400 dari webhook karena payload tidak valid, atau permintaan laporan untuk akun yang dihapus.
Backoff adalah jeda antar percobaan. Linear backoff (5s, 10s, 15s) sederhana, tapi masih bisa menciptakan gelombang trafik. Exponential backoff (5s, 10s, 20s, 40s) menyebarkan beban lebih baik dan biasanya lebih aman untuk webhook dan penyedia pihak ketiga. Tambahkan jitter (delay acak kecil) agar ribuan job tidak retry di detik yang sama setelah gangguan.
Aturan yang cenderung berperilaku baik di produksi:
Max attempts bertujuan membatasi kerusakan. Untuk banyak tim, 5 sampai 8 percobaan sudah cukup. Setelah itu, berhenti retry dan letakkan job untuk ditinjau (alur dead-letter) daripada berputar selamanya.
Timeout mencegah job "zombie". Email mungkin timeout dalam 10–20 detik per percobaan. Webhook sering membutuhkan batas lebih pendek, seperti 5–10 detik, karena penerima mungkin down dan Anda ingin melanjutkan. Pembuatan laporan boleh memakan menit, tapi tetap harus ada cutoff keras.
Jika Anda membangun ini di Koder.ai, anggap should_retry, next_run_at, dan idempotency key sebagai field kelas satu. Detail kecil itu menjaga sistem tetap tenang saat ada masalah.
Status dead-letter adalah tempat job masuk saat retry tidak lagi aman atau berguna. Ini mengubah kegagalan senyap menjadi sesuatu yang bisa Anda lihat, cari, dan tindaklanjuti.
Simpan cukup konteks untuk memahami apa yang terjadi dan memutar ulang job tanpa menebak, tapi berhati-hatilah terhadap rahasia.
Simpan:
Jika payload berisi token atau data pribadi, redaksi atau enkripsi sebelum menyimpan.
Saat job masuk dead-letter, buat keputusan cepat: retry, perbaiki, atau abaikan.
Retry untuk outage eksternal dan timeout. Perbaiki untuk data buruk (email hilang, URL webhook salah) atau bug di kode Anda. Abaikan sebaiknya jarang, tapi valid saat job tidak relevan lagi (mis. pelanggan menghapus akun). Jika mengabaikan, catat alasannya agar job tidak terlihat lenyap.
Requeue manual paling aman jika membuat job baru dan membuat yang lama immutable. Tandai job dead-letter dengan siapa yang mengantri ulang, kapan, dan kenapa, lalu enqueue salinan baru dengan ID baru.
Untuk alerting, pantau sinyal yang biasanya berarti masalah nyata: jumlah dead-letter naik cepat, error yang sama muncul berulang di banyak job, dan job antre lama yang tidak diklaim.
Jika Anda menggunakan Koder.ai, snapshot dan rollback membantu saat rilis buruk tiba-tiba meningkatkan kegagalan, karena Anda bisa membatalkan perubahan cepat saat menyelidiki.
Terakhir, tambahkan katup pengaman untuk outage vendor. Batasi pengiriman per penyedia, dan gunakan circuit breaker: jika endpoint webhook gagal parah, jeda percobaan baru selama jangka pendek agar Anda tidak membanjiri server mereka (dan server Anda sendiri).
Antrean bekerja terbaik ketika setiap tipe job punya aturan jelas: apa yang dihitung sebagai sukses, apa yang harus diretry, dan apa yang tidak boleh terjadi dua kali.
Email. Kebanyakan kegagalan email bersifat sementara: timeout penyedia, rate limit, atau outage singkat. Perlakukan itu sebagai retryable, dengan backoff. Risiko lebih besar adalah pengiriman duplikat, jadi buat job email idempotent. Simpan kunci dedupe stabil seperti user_id + template + event_id dan tolak mengirim jika kunci itu sudah ditandai sebagai terkirim.
Juga berguna menyimpan nama dan versi template (atau hash subject/body yang dirender). Jika nanti perlu menjalankan ulang job, Anda bisa memilih apakah mengirim ulang konten yang sama persis atau merender ulang dari template terbaru. Jika penyedia mengembalikan message ID, simpan agar dukungan bisa melacak kejadian.
Laporan. Laporan gagal dengan cara berbeda. Mereka bisa berjalan berjam-jam, mencapai limit pagination, atau kehabisan memori jika melakukan semuanya sekaligus. Pecah pekerjaan menjadi bagian kecil. Pola umum: satu job "request laporan" membuat banyak job "page" (atau "chunk"), masing-masing memproses irisan data.
Simpan hasil untuk diunduh nanti daripada membuat pengguna menunggu. Itu bisa berupa tabel database yang di-key oleh report_run_id, atau referensi file plus metadata (status, jumlah baris, created_at). Tambahkan field progres agar UI bisa menunjukkan "processing" vs "ready" tanpa menebak.
Webhooks. Webhook soal keandalan pengiriman, bukan kecepatan. Tanda tangani setiap request (mis. HMAC dengan shared secret) dan sertakan timestamp untuk mencegah replay. Retry hanya saat penerima mungkin berhasil nanti.
Aturan sederhana:
Ordering dan prioritas. Sebagian besar job tidak memerlukan ordering ketat. Saat urutan penting, biasanya per key (per user, per invoice, per endpoint webhook). Tambahkan group_key dan jalankan hanya satu job dalam penerbangan per key.
Untuk prioritas, pisahkan pekerjaan mendesak dari pekerjaan lambat. Backlog laporan besar tidak boleh menunda email reset password.
Contoh: setelah pembelian, Anda enqueue (1) email konfirmasi order, (2) webhook ke partner, dan (3) job pembaruan laporan. Email bisa retry cepat, webhook retry lebih lama dengan backoff, dan laporan jalan nanti dengan prioritas rendah.
Seorang pengguna mendaftar di aplikasi Anda. Tiga hal harus terjadi, tapi tidak satu pun harus memperlambat halaman signup: kirim email selamat datang, beri tahu CRM lewat webhook, dan masukkan pengguna ke laporan aktivitas malam.
Tepat setelah membuat record user, tulis tiga baris job ke antrean database Anda. Masing-masing baris punya tipe, payload (seperti user_id), status, jumlah percobaan, dan timestamp next_run_at.
Siklus tipikal terlihat seperti ini:
queued: dibuat dan menunggu workerrunning: worker telah mengklaimnyasucceeded: selesai, tidak ada kerja lagifailed: gagal, dijadwalkan lagi atau kehabisan retrydead: gagal berkali-kali dan butuh perhatian manusiaJob email selamat datang menyertakan idempotency key seperti welcome_email:user:123. Sebelum mengirim, worker memeriksa tabel kunci idempotensi yang selesai (atau menegakkan constraint unik). Jika job berjalan dua kali karena crash, percobaan kedua melihat kunci dan melewatkan pengiriman. Tidak ada email selamat datang ganda.
Sekarang endpoint CRM webhook down. Job webhook gagal karena timeout. Worker Anda menjadwalkan retry menggunakan backoff (mis. 1 menit, 5 menit, 30 menit, 2 jam) plus sedikit jitter agar banyak job tidak retry di detik yang sama.
Setelah maksimal percobaan, job menjadi dead. Pengguna tetap mendaftar, mendapat email selamat datang, dan job laporan malam bisa berjalan normal. Hanya notifikasi CRM yang terjebak, dan itu terlihat.
Keesokan paginya, support (atau on-call) bisa menanganinya tanpa menggali log berjam-jam:
webhook.crm).Jika Anda membangun aplikasi di platform seperti Koder.ai, pola yang sama berlaku: jaga alur pengguna cepat, dorong efek samping ke job, dan buat kegagalan mudah diperiksa dan dijalankan ulang.
Cara tercepat merusak antrean adalah menganggapnya opsional. Tim sering mulai dengan "kirim email di request kali ini saja" karena terasa lebih sederhana. Lalu menyebar: reset password, struk, webhook, export laporan. Lama-kelamaan aplikasi terasa lambat, timeout naik, dan gangguan pihak ketiga menjadi outage bagi Anda.
Perangkap lain adalah melewatkan idempotensi. Jika job bisa berjalan dua kali, ia tidak boleh menghasilkan dua hasil. Tanpa idempotensi, retry berubah jadi email ganda, event webhook berulang, atau lebih buruk.
Masalah ketiga adalah visibilitas. Jika Anda hanya tahu tentang kegagalan dari tiket support, antrean sudah merugikan pengguna. Bahkan tampilan internal dasar yang menunjukkan jumlah job per status plus last_error yang dapat dicari sangat menghemat waktu.
Beberapa masalah muncul cepat, bahkan di antrean sederhana:
Backoff mencegah outage buatan sendiri. Bahkan jadwal dasar seperti 1 menit, 5 menit, 30 menit, 2 jam membuat kegagalan lebih aman. Juga set batas percobaan sehingga job rusak berhenti dan menjadi terlihat.
Jika Anda membangun di platform seperti Koder.ai, ada baiknya mengirimkan dasar-dasar ini bersama fitur, bukan beberapa minggu kemudian sebagai proyek pembersihan.
Sebelum Anda menambahkan tooling, pastikan dasar-dasarnya kuat. Antrean berbasis database bekerja baik bila setiap job mudah diklaim, mudah diretry, dan mudah diperiksa.
Daftar periksa reliabilitas cepat:
Selanjutnya, pilih tiga tipe job pertama Anda dan tuliskan aturannya. Contoh: email reset password (retry cepat, max singkat), laporan malam (sedikit retry, timeout lebih lama), pengiriman webhook (lebih banyak retry, backoff lebih lama, berhenti pada 4xx permanen).
Jika Anda ragu kapan antrean database tidak lagi cukup, pantau sinyal seperti kontensi tingkat baris dari banyak worker, kebutuhan ordering ketat di banyak tipe job, fan-out besar (satu event memicu ribuan job), atau konsumsi lintas-layanan di mana tim berbeda memiliki worker masing-masing.
Jika Anda ingin prototipe cepat, Anda bisa merancang alur di Koder.ai (koder.ai) menggunakan planning mode, menghasilkan tabel jobs dan loop worker, dan iterasi dengan snapshot dan rollback sebelum deploy.
Jika sebuah tugas bisa membutuhkan lebih dari satu atau dua detik, atau bergantung pada panggilan jaringan (penyedia email, endpoint webhook, query lambat), pindahkan ke pekerjaan latar belakang.
Fokuskan request pengguna pada validasi input, menulis perubahan data utama, memasukkan job ke antrean, dan mengembalikan respons cepat.
Mulailah dengan antrean berbasis database ketika:
Tambahkan broker atau alat streaming nanti saat Anda membutuhkan throughput sangat tinggi, banyak konsumen independen, atau replay event antar-layanan.
Lacak dasar yang menjawab: apa yang harus dilakukan, kapan mencoba lagi, dan apa yang terjadi terakhir kali.
Minimum praktis:
Simpan input, bukan output besar.
Payload yang baik:
user_id, template, report_id)Hindari:
Kuncinya adalah langkah “claim” atomik sehingga dua worker tidak bisa mengambil job yang sama.
Pendekatan umum di Postgres:
FOR UPDATE SKIP LOCKED)running dan set locked_at/locked_byDengan begitu worker bisa skala horizontal tanpa memproses ganda baris yang sama.
Asumsikan job bisa berjalan dua kali (crash, timeout, retry). Buat efek sampingnya aman.
Polanya sederhana:
idempotency_key seperti welcome_email:user:123Ini penting untuk email dan webhook agar tidak terjadi duplikasi.
Gunakan kebijakan default yang jelas dan sederhana:
Gagal cepat pada error permanen (mis. alamat email hilang, payload tidak valid, kebanyakan 4xx webhook).
Dead-letter berarti “hentikan retry dan buat terlihat.” Gunakan ketika:
max_attemptsSimpan konteks yang cukup untuk bertindak:
Tangani job “stuck running” dengan dua aturan:
running yang lebih tua dari ambang dan mengantri ulangnya (atau menandainya gagal)Ini memungkinkan sistem pulih dari crash worker tanpa pembersihan manual.
Pisahkan agar pekerjaan lambat tidak memblokir pekerjaan penting:
Jika urutan penting, biasanya per-kunci (per user, per endpoint webhook). Tambahkan group_key dan pastikan hanya satu job berjalan per key untuk mempertahankan urutan lokal tanpa memaksa urutan global.
job_type, payloadstatus (queued, running, succeeded, failed, dead)attempts, max_attemptsnext_run_at, plus created_atlocked_at, locked_bylast_erroridempotency_key (atau mekanisme dedupe lain)Jika job butuh data besar, simpan referensi (mis. report_run_id atau file key) dan ambil konten saat worker berjalan.
last_error dan kode status terakhir (untuk webhook)Saat replay, lebih baik buat job baru dan biarkan job dead-letter tetap tak berubah.