Ada pekerjaan yang memang tidak boleh membuat pengguna menunggu: mengirim email, membuat PDF, memanggil API pihak ketiga yang lambat, atau memproses video. Jika pekerjaan seperti itu dijalankan di dalam request, waktu respons aplikasi Anda ditentukan oleh tugas paling lambat di dalamnya — dan pengguna tidak peduli bahwa PDF memang butuh waktu lima detik. Background job memindahkan pekerjaan tersebut keluar dari request: aplikasi hanya menitipkan pekerjaan, dan proses terpisah menyelesaikannya nanti. Chapter ini membahas konsep dasarnya secara utuh — bentuk sebuah job, peran dispatch dan worker, serta demo queue sederhana berbasis database — sebagai fondasi seluruh Section Asynchronous Processing.
Masalah: Tugas Lambat Menghambat Request #
Mari kita hitung dampaknya secara konkret. Asumsikan sebuah endpoint checkout menyelesaikan tiga tugas berat secara berurutan di dalam request: mengirim email konfirmasi (1,2 detik), membuat PDF invoice (2,5 detik), dan men-ganti ukuran foto produk (0,8 detik). Total durasi tugas tersebut menjadi beban langsung waktu respons, sedangkan versi berbasis queue hanya menanggung biaya menitipkan job — sekitar 5 milidetik per job:
<?php
$tugas = [
["nama" => "kirim email konfirmasi", "durasi_ms" => 1200],
["nama" => "buat PDF invoice", "durasi_ms" => 2500],
["nama" => "resize foto produk", "durasi_ms" => 800],
];
$totalSinkron = array_sum(array_column($tugas, "durasi_ms"));
$totalQueue = count($tugas) * 5;
echo "Total durasi tugas : {$totalSinkron} ms\n";
echo "Respons sinkron : {$totalSinkron} ms\n";
echo "Respons dengan queue: {$totalQueue} ms\n";Output yang Dihasilkan:
Total durasi tugas : 4500 ms
Respons sinkron : 4500 ms
Respons dengan queue: 15 msAngka array_sum() menunjukkan penderitaan versi sinkron: pengguna menatap spinner selama 4,5 detik padahal satu-satunya yang ia butuhkan adalah konfirmasi bahwa pesanan diterima. Versi queue menanggung 15 milidetik untuk tiga job, lalu mengembalikan respons — pekerjaan berat tetap selesai, hanya saja setelah respons terkirim. Selisih 4,5 detik versus 15 milidetik inilah alasan pertama pola background job ada; alasan kedua lebih penting lagi: kegagalan pengiriman email tidak lagi membuat checkout gagal.
Job sebagai Tipe dan Payload #
Apa persisnya yang dititipkan ke queue? Jawabannya: job — struktur data kecil berisi tipe pekerjaan dan payload (data yang dibutuhkan). Worker membaca tipe untuk memutuskan kode mana yang dijalankan, lalu membaca payload untuk mengetahui detailnya. Bentuk yang paling portabel adalah JSON, dan PHP mengubahnya dua arah dengan json_encode() serta json_decode():
<?php
$job = [
"type" => "kirim_email",
"payload" => [
"recipient" => "[email protected]",
"subject" => "Verifikasi Akun",
"template" => "verifikasi-akun",
],
"attempts" => 0,
];
$encoded = json_encode($job, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
echo $encoded . "\n";
$decoded = json_decode($encoded, true);
echo "Tipe job : {$decoded['type']}\n";
echo "Penerima : {$decoded['payload']['recipient']}\n";Output yang Dihasilkan:
{"type":"kirim_email","payload":{"recipient":"[email protected]","subject":"Verifikasi Akun","template":"verifikasi-akun"},"attempts":0}
Tipe job : kirim_email
Penerima : [email protected]Struktur di atas memisahkan tiga hal penting: type menentukan handler yang akan dieksekusi worker, payload membawa data domain (penerima, subject, nama template), dan attempts mencatat berapa kali job pernah dicoba — kolom yang sama dengan yang kita gunakan pada retry email. Payload hanya berisi data, bukan resource atau object hidup; koneksi database, file handle, dan closure tidak bisa diserialisasi lintas proses. Karena itu handler job selalu membangun ulang resource yang dibutuhkannya dari payload, bukan mewarisinya dari request.
Dispatch, Worker, dan Consumer #
Dua belah pihak berkomunikasi melalui queue, masing-masing dengan nama peran yang perlu Anda kenali sejak awal. Dispatcher (disebut juga producer) berjalan di dalam request: dispatcher membuat job, menserialisasinya, lalu menyimpannya ke queue. Worker (disebut juga consumer) berjalan sebagai proses terpisah: worker mengambil job dari queue, mengeksekusi handler yang cocok dengan tipe job, lalu mencatat hasilnya. Queue sendiri hanyalah media penyimpanan berurutan — bisa berupa tabel database, Redis, atau message broker.
Diagram tersebut memperlihatkan dua alur yang berjalan pada waktu berbeda: alur atas milik request (dispatch lalu respons segera) dan alur bawah milik worker yang berputar terus mengambil job. Tidak ada panah dari worker kembali ke request — hasil pekerjaan tidak dikirim sebagai respons, melainkan disimpan (status order berubah, berkas PDF tersedia) dan pengguna melihatnya saat memuat halaman berikutnya. Konsekuensi desain ini penting dipahami: pengguna perlu cara memantau kemajuan, misalnya halaman status pesanan yang membaca kolom status dari database.
PHP Shared-Nothing dan Worker Eksternal #
PHP web memiliki model eksekusi shared-nothing: setelah request selesai, interpreter membebaskan seluruh memori, menutup koneksi database, dan menghapus semua variabel. Model tersebut menjadikan PHP tangguh (kebocoran memori tidak menumpuk), tetapi juga berarti tidak ada variabel yang bisa “disimpan untuk nanti” di dalam memori proses web. Inilah alasan queue selalu berupa penyimpanan eksternal — tabel database, Redis, atau message broker — dan worker selalu berupa proses PHP terpisah yang hidupnya tidak bergantung pada request.
Worker eksternal tersebut biasanya dijalankan sebagai daemon lewat manajer proses (systemd, supervisor) dengan perintah semacam php worker.php, dan di-restart otomatis bila crash. Antarmuka baris perintah PHP (CLI SAPI) mendukung pola ini dengan baik: script CLI berjalan tanpa batas waktu default, bisa membaca argumen, dan bisa dihentikan dengan sinyal. Ketika satu worker tidak cukup, Anda menjalankan beberapa worker sekaligus — itulah mengapa desain job yang akan kita lihat berikutnya harus aman terhadap beberapa pengambil bersamaan.
Queue Sederhana Berbasis Database #
Cara tercepat memahami queue adalah membangunnya. Program berikut membuat queue pada SQLite :memory: dengan tiga fungsi inti: pushJob() menitipkan job, popNextJob() mengambil job tertua yang masih pending, dan loop utama memproses sampai queue kosong:
<?php
$pdo = new PDO("sqlite::memory:");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
$pdo->exec("CREATE TABLE jobs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
type TEXT NOT NULL,
payload TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending'
)");
function pushJob(PDO $pdo, string $type, array $payload): int
{
$stmt = $pdo->prepare("INSERT INTO jobs (type, payload) VALUES (:type, :payload)");
$stmt->execute([":type" => $type, ":payload" => json_encode($payload)]);
return (int) $pdo->lastInsertId();
}
function popNextJob(PDO $pdo): ?array
{
$stmt = $pdo->prepare(
"UPDATE jobs SET status = 'processing'
WHERE id = (SELECT id FROM jobs WHERE status = 'pending' ORDER BY id LIMIT 1)
RETURNING id, type, payload"
);
$stmt->execute();
$job = $stmt->fetch();
if ($job === false) {
return null;
}
$job["payload"] = json_decode($job["payload"], true);
return $job;
}
pushJob($pdo, "kirim_email", ["recipient" => "[email protected]"]);
pushJob($pdo, "buat_pdf", ["orderId" => 1042]);
pushJob($pdo, "resize_foto", ["file" => "produk-01.jpg"]);
while (($job = popNextJob($pdo)) !== null) {
echo "Worker memproses job #{$job['id']} [{$job['type']}]: " . json_encode($job["payload"]) . "\n";
$pdo->prepare("UPDATE jobs SET status = 'done' WHERE id = :id")->execute([":id" => $job["id"]]);
}
$sisa = (int) $pdo->query("SELECT COUNT(*) FROM jobs WHERE status = 'pending'")->fetchColumn();
echo "Job pending tersisa: {$sisa}\n";Output yang Dihasilkan:
Worker memproses job #1 [kirim_email]: {"recipient":"[email protected]"}
Worker memproses job #2 [buat_pdf]: {"orderId":1042}
Worker memproses job #3 [resize_foto]: {"file":"produk-01.jpg"}
Job pending tersisa: 0Mari kita bedah komponen-komponennya:
pushJob()menserialisasi payload denganjson_encode()lalu menyimpan baris baru berstatuspending. Primary keyAUTOINCREMENTmemberi setiap job nomor urut yang monoton naik.popNextJob()mengambil job tertua lewat subqueryORDER BY id LIMIT 1sekaligus menandainyaprocessingdalam satu statementUPDATE ... RETURNING— teknik claim atomik yang sama dengan queue email pada Section Email dan Notifications, sehingga dua worker yang berjalan bersamaan tidak akan mendapat job yang sama.- Urutan hasil pada output membuktikan sifat FIFO (first in, first out): job diproses persis sesuai urutan penitipan
#1,#2,#3, karena kolomidyang menjadi dasar pengurutan bertambah setiap push. - Loop
whileberhenti saatpopNextJob()mengembalikannull— sinyal queue kosong; di worker daemon sesungguhnya, titik ini diganti jeda tidur lalu loop dilanjutkan.
Menjalankan Proses Latar Belakang Nyata #
Teori worker terasa lebih nyata ketika Anda melihat proses PHP kedua benar-benar berjalan. Fungsi proc_open() memulai proses baru dari script PHP kita sendiri; program induk tidak perlu menunggu proses anak selesai untuk melanjutkan eksekusinya. Demo berikut menitipkan “pekerjaan” kepada proses anak yang menulis hasil ke berkas sementara:
<?php
$tempFile = tempnam(sys_get_temp_dir(), "jobdemo_");
$command = [
PHP_BINARY,
"-r",
'file_put_contents($argv[1], "laporan penjualan selesai\n");',
$tempFile,
];
$pipes = [];
$process = proc_open($command, [["pipe", "r"], ["pipe", "w"], ["pipe", "w"]], $pipes);
if (is_resource($process)) {
echo "Request pengguna langsung kembali tanpa menunggu worker\n";
proc_close($process);
echo "Worker menulis: " . trim((string) file_get_contents($tempFile)) . "\n";
}
unlink($tempFile);Output yang Dihasilkan:
Request pengguna langsung kembali tanpa menunggu worker
Worker menulis: laporan penjualan selesaiArray $command memakai konstanta PHP_BINARY (path interpreter yang sedang berjalan) sehingga demo bekerja di komputer mana pun, lalu opsi -r menjalankan potongan kode pendek tanpa berkas script terpisah — kode anak menulis baris tetap ke berkas yang path-nya dikirim lewat $argv[1]. Pada contoh ini proc_close() dipanggil agar kita bisa memeriksa hasilnya secara deterministik; di skenario nyata, proses anak dibiarkan berjalan dan induknya segera mengembalikan respons. Berkas sementara dibuat lewat tempnam() di direktori sistem lalu dihapus dengan unlink() agar tidak menyisakan sampah.
Kapan Tidak Perlu Queue #
Background job bukan obat untuk segalanya, dan kompleksitasnya nyata: ada komponen tambahan yang harus diawasi, kegagalan job tidak lagi terlihat dari request, dan alur program terpotong menjadi dua tahap. Jika aplikasi Anda menerima sepuluh permintaan per menit dan setiap request hanya butuh beberapa puluh milidetik, menjalankan semuanya secara sinkron lebih sederhana untuk dibangun, diuji, dan diperbaiki. Panduan praktisnya: pertimbangkan queue ketika ada tugas yang lambat (ratusan milidetik ke atas), boleh selesai setelah respons, atau perlu di-retry secara otomatis. Email verifikasi, pembuatan dokumen, dan pemrosesan berkas memenuhi ketiga kriteria tersebut; validasi form dan pembacaan data biasa tidak.
Ringkasan #
Background job memindahkan pekerjaan berat keluar dari request sehingga pengguna mendapat respons dalam milidetik sementara pekerjaan selesai di latar belakang. Sebuah job adalah pasangan tipe dan payload yang diserialisasi — data murni tanpa resource hidup — dan dialirkan dari dispatcher di request menuju worker daemon melalui queue eksternal, karena model shared-nothing PHP tidak menyimpan apa pun di antara request. Queue berbasis database dengan claim atomik UPDATE ... RETURNING memberi jaminan FIFO yang bisa Anda bangun dan uji sendiri. Fondasi ini cukup untuk memahami komponen queue secara lebih dalam — prioritas, delayed job, dan queue pada framework — yang menjadi fokus chapter berikutnya.