September 27, 2026

Laravel Queue.

Menjalankan pekerjaan berat di background dengan Laravel Queue: job, driver, worker, retry & backoff, failed jobs, batching, Horizon, dan Supervisor untuk produksi.

Mengirim email, memproses gambar, memanggil API pihak ketiga, atau membuat laporan PDF tidak perlu membuat user menunggu. Dengan queue, request cukup “menitipkan” pekerjaan (job), lalu proses terpisah (worker) mengerjakannya di background. Catatan ini memakai sintaks Laravel 13.


1. Konsep

Request HTTP ──dispatch()──▶ [ Queue: database / redis / sqs ] ──▶ Worker (queue:work) ──▶ handle()
      │                                                                  │
      └── langsung response ke user                                      └── retry / failed_jobs
IstilahArti
JobClass berisi pekerjaan (handle())
ConnectionBackend penyimpan antrean (database, redis, sqs, sync)
QueueNama antrean di dalam satu connection (default, emails, high)
WorkerProses PHP jangka panjang yang mengambil & menjalankan job

2. Driver

Set di .env:

QUEUE_CONNECTION=database
DriverKelebihanCatatan
syncLangsung dijalankan (tanpa antrean)Untuk testing/debug saja
databaseTanpa dependensi tambahan, default di project baruCukup untuk trafik kecil–menengah
redisCepat, mendukung HorizonButuh Redis
sqsManaged oleh AWS, skalabelAda batas delay 15 menit
nullMembuang jobMenonaktifkan queue

Project Laravel baru sudah menyertakan migrasi tabel jobs. Jika belum ada:

php artisan make:queue-table
php artisan migrate

3. Membuat & Dispatch Job

php artisan make:job SendInvoiceEmail
<?php

namespace App\Jobs;

use App\Mail\InvoiceMail;
use App\Models\Order;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Mail;

class SendInvoiceEmail implements ShouldQueue
{
    use Queueable;

    public function __construct(public Order $order) {}

    public function handle(): void
    {
        Mail::to($this->order->user)->send(new InvoiceMail($this->order));
    }
}

Model yang dikirim ke constructor diserialisasi hanya ID-nya, lalu di-query ulang saat job dijalankan — jadi datanya selalu segar dan payload kecil.

Dispatch:

SendInvoiceEmail::dispatch($order);

SendInvoiceEmail::dispatch($order)
    ->onQueue('emails')
    ->delay(now()->addMinutes(5));

SendInvoiceEmail::dispatchIf($order->isPaid(), $order);
SendInvoiceEmail::dispatchSync($order);           // jalankan sekarang, tanpa antrean

// Tunggu transaksi DB commit dulu, supaya job tidak membaca data yang belum tersimpan
SendInvoiceEmail::dispatch($order)->afterCommit();

Laravel 13 menambahkan queue routing terpusat, jadi job class tidak perlu tahu queue-nya:

// AppServiceProvider::boot()
use Illuminate\Support\Facades\Queue;

Queue::route(SendInvoiceEmail::class, connection: 'redis', queue: 'emails');

Mailable, Notification, dan Event Listener juga bisa di-queue dengan implements ShouldQueue — tidak harus selalu membuat Job sendiri.


4. Menjalankan Worker

php artisan queue:work                          # connection default, queue "default"
php artisan queue:work redis --queue=high,default,emails   # urutan = prioritas
php artisan queue:work --tries=3 --timeout=90 --sleep=3
php artisan queue:work --max-jobs=500 --max-time=3600      # restart berkala (cegah memory leak)
php artisan queue:listen                        # reload kode tiap job; lambat, untuk dev

Worker adalah proses jangka panjang yang memuat kode sekali di awal. Setelah deploy kode baru, worker lama masih menjalankan kode lama, jadi selalu jalankan:

php artisan queue:restart

Di development, composer run dev pada project baru sudah menjalankan server, Vite, log, dan queue:listen sekaligus.


5. Retry, Backoff & Timeout

Laravel 13 bisa memakai attribute atau property klasik:

use Illuminate\Queue\Attributes\Backoff;
use Illuminate\Queue\Attributes\Timeout;
use Illuminate\Queue\Attributes\Tries;

#[Tries(5)]
#[Timeout(120)]
#[Backoff(delay: 10, maxDelay: 60, multiplier: 2)]
class SyncToPaymentGateway implements ShouldQueue
{
    use Queueable;
    // ...
}
// Cara klasik
class SyncToPaymentGateway implements ShouldQueue
{
    public $tries = 5;
    public $timeout = 120;
    public $backoff = [10, 30, 60];   // jeda per percobaan (detik)

    // atau batasi berdasarkan waktu, bukan jumlah percobaan
    public function retryUntil(): \DateTime
    {
        return now()->addMinutes(30);
    }
}
PengaturanFungsi
triesJumlah percobaan maksimum sebelum job dianggap gagal
backoffJeda sebelum retry — hindari membombardir API yang sedang down
timeoutBatas waktu satu eksekusi; worker membunuh job yang melewati ini
maxExceptionsGagal lebih cepat jika exception (bukan release manual) terlalu banyak

Aturan penting: timeout harus lebih kecil dari retry_after di config/queue.php. Jika tidak, job yang masih berjalan dianggap hilang dan diambil worker lain → job dieksekusi dua kali.


6. Failed Jobs

Job yang habis percobaannya masuk ke tabel failed_jobs.

use Throwable;

public function failed(?Throwable $exception): void
{
    // beri tahu admin, ubah status order, dsb.
}
php artisan queue:failed               # daftar
php artisan queue:retry all            # coba ulang semua
php artisan queue:retry 5f3c...        # coba ulang satu UUID
php artisan queue:forget 5f3c...
php artisan queue:flush                # hapus semua failed job
php artisan queue:prune-failed --hours=48

7. Batching & Chaining

Chain: job dijalankan berurutan; jika satu gagal, sisanya berhenti.

use Illuminate\Support\Facades\Bus;

Bus::chain([
    new GenerateInvoicePdf($order),
    new SendInvoiceEmail($order),
    new MarkInvoiceSent($order),
])->dispatch();

Batch: banyak job paralel dengan callback ketika selesai. Butuh tabel batch dan trait Batchable.

php artisan make:queue-batches-table
php artisan migrate
use Illuminate\Bus\Batch;
use Illuminate\Bus\Batchable;
use Throwable;

class ImportCsvChunk implements ShouldQueue
{
    use Batchable, Queueable;

    public function handle(): void
    {
        if ($this->batch()?->cancelled()) {
            return;
        }
        // proses potongan CSV...
    }
}

$batch = Bus::batch($chunks->map(fn ($rows) => new ImportCsvChunk($rows)))
    ->then(fn (Batch $batch) => logger('Import selesai'))
    ->catch(fn (Batch $batch, Throwable $e) => logger()->error($e->getMessage()))
    ->finally(fn (Batch $batch) => /* bersihkan file */ null)
    ->name('Import produk')
    ->dispatch();

// progres untuk ditampilkan di UI
Bus::findBatch($batch->id)->progress();   // 0–100

8. Job Middleware & Unique Job

use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Queue\Middleware\RateLimited;
use Illuminate\Queue\Middleware\WithoutOverlapping;

class RebuildSearchIndex implements ShouldQueue, ShouldBeUnique
{
    use Queueable;

    public function __construct(public Product $product) {}

    public function uniqueId(): string
    {
        return (string) $this->product->id;   // hanya satu job per produk di antrean
    }

    public function middleware(): array
    {
        return [new WithoutOverlapping($this->product->id)];
    }
}

9. Horizon (untuk Redis)

Laravel Horizon adalah dashboard + manajer worker untuk queue berbasis Redis: throughput, runtime, failed job, dan auto-balancing jumlah worker per queue, dikonfigurasi lewat kode (config/horizon.php).

composer require laravel/horizon
php artisan horizon:install
php artisan horizon          # menggantikan queue:work
# dashboard di /horizon (atur akses lewat gate viewHorizon)
php artisan horizon:terminate   # setelah deploy

10. Produksi dengan Supervisor

Worker harus dijaga agar selalu hidup dan di-restart jika mati. Di server Linux, gunakan Supervisor:

sudo apt install supervisor

/etc/supervisor/conf.d/laravel-worker.conf:

[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/app/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=4
redirect_stderr=true
stdout_logfile=/var/www/app/storage/logs/worker.log
stopwaitsecs=3600
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start "laravel-worker:*"
sudo supervisorctl status

Untuk Horizon, command cukup php /var/www/app/artisan horizon dengan numprocs=1. Di Docker, jalankan worker sebagai service terpisah (lihat Docker Compose untuk Dev Environment ). stopwaitsecs harus lebih lama dari job terlama agar job tidak terpotong saat restart.


11. Pitfalls

  • Lupa queue:restart setelah deploy → worker masih pakai kode lama.
  • timeout ≥ retry_after → job dobel.
  • Job tidak idempoten → retry bisa mengirim email dua kali atau menagih dua kali. Simpan status/flag sebelum aksi yang punya efek samping.
  • Dispatch di dalam transaksi DB → worker membaca data yang belum di-commit. Pakai afterCommit() (atau after_commit => true di config).
  • Mengirim objek besar/closure ke constructor → payload membengkak. Kirim model atau ID saja.
  • QUEUE_CONNECTION=sync terbawa ke produksi → semua job berjalan di dalam request, lambat.
  • Worker di Supervisor jalan sebagai root → file log/cache milik root, web server tidak bisa menulis. Set user=www-data.

Queue dan retry otomatis menjadi tulang punggung provisioning di Web Host Manager . Catatan terkait: Eloquent ORM , Cron Basics .

Referensi: Queues , Horizon .

Hey! I’m Fanny, the software engineer tending to this digital garden. You can read more about me, or subscribe by email.

Comments