Queue dan Job di Laravel: Arsitektur Asinkron dan Pertanyaan Wawancara 2026

Pembahasan mendalam tentang arsitektur queue dan job di Laravel. Mencakup dispatch job, batching, chaining, middleware, penanganan job gagal, dan manajemen queue worker dengan contoh Laravel 12.

Diagram arsitektur asinkron queue dan job Laravel dengan proses worker dan pipeline dispatch job

Queue Laravel menyediakan API terpadu untuk menunda tugas yang memakan waktu — pengiriman email, pemrosesan upload, pembuatan laporan — ke worker latar belakang. Daripada memaksa pengguna menunggu, aplikasi mendorong job ke queue dan melanjutkan pekerjaannya. Mekanisme ini menjadi inti dari setiap aplikasi Laravel yang skalabel.

Arsitektur Queue Sekilas

Laravel mendukung berbagai backend queue (Redis, Amazon SQS, database, Beanstalkd) melalui satu API yang independen dari driver. Job adalah class PHP terserialisasi yang mengimplementasikan interface ShouldQueue. Worker menarik job dari queue, melakukan deserialisasi, dan menjalankan method handle(). Job yang gagal akan masuk ke tabel failed_jobs untuk dicoba ulang atau diperiksa.

Cara Kerja Job Dispatching Laravel di Balik Layar

Saat dispatch() dipanggil pada class job, Laravel melakukan serialisasi instance job — termasuk properti publiknya — dan mendorong payload ke connection queue yang dikonfigurasi. Payload terserialisasi berisi nama class lengkap, properti yang diserialisasi, nama queue target, serta metadata seperti jumlah percobaan yang diizinkan dan timeout.

Proses worker (php artisan queue:work) berjalan sebagai daemon berumur panjang yang melakukan polling backend queue untuk job baru. Setelah menerima payload, worker melakukan deserialisasi job, menyelesaikan dependencies melalui service container, lalu memanggil handle().

App/Jobs/ProcessInvoice.phpphp
namespace App\Jobs;

use App\Models\Order;
use App\Services\PdfGenerator;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class ProcessInvoice implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 3;
    public int $backoff = 60;
    public int $timeout = 120;

    public function __construct(
        public readonly Order $order
    ) {}

    public function handle(PdfGenerator $pdf): void
    {
        // Generate PDF invoice for the order
        $invoice = $pdf->generate($this->order);

        // Store the generated file
        $this->order->update([
            'invoice_path' => $invoice->path(),
            'invoiced_at' => now(),
        ]);
    }

    public function failed(\Throwable $e): void
    {
        // Notify the ops team when invoice generation fails
        logger()->error('Invoice generation failed', [
            'order_id' => $this->order->id,
            'error' => $e->getMessage(),
        ]);
    }
}

Trait SerializesModels hanya menyimpan primary key dan nama class model, bukan keseluruhan model Eloquent. Ketika worker memproses job, ia mengambil model segar dari database. Pendekatan ini menghindari data usang dan menjaga ukuran payload tetap kecil.

Job Batching untuk Beban Kerja Paralel

Job batching mengelompokkan beberapa job ke dalam satu batch, melacak progres kolektif, dan memicu callback ketika semua job selesai — atau ketika salah satu gagal. Pola ini cocok untuk impor data, notifikasi massal, dan pembuatan laporan ketika beberapa unit kerja independen harus selesai sebelum langkah final dijalankan.

App/Http/Controllers/ImportController.phpphp
use App\Jobs\ImportRow;
use Illuminate\Bus\Batch;
use Illuminate\Support\Facades\Bus;

public function import(Request $request)
{
    $rows = $this->parseCSV($request->file('data'));

    // Create a batch of import jobs, one per CSV row
    $batch = Bus::batch(
        collect($rows)->map(fn ($row) => new ImportRow($row))
    )
    ->then(function (Batch $batch) {
        // All jobs completed successfully
        Notification::send(
            auth()->user(),
            new ImportComplete($batch->totalJobs)
        );
    })
    ->catch(function (Batch $batch, \Throwable $e) {
        // First failure in the batch
        logger()->warning('Batch import partial failure', [
            'batch_id' => $batch->id,
            'failed' => $batch->failedJobs,
        ]);
    })
    ->finally(function (Batch $batch) {
        // Runs after all jobs finish (success or failure)
        Cache::forget("import_lock_{$batch->id}");
    })
    ->allowFailures()
    ->dispatch();

    return response()->json(['batch_id' => $batch->id]);
}

Laravel 12 memperkaya payload batch dengan metadata, termasuk waktu tunggu queue dan identifikasi worker. Method allowFailures() mencegah satu job gagal membatalkan seluruh batch — penting untuk impor besar di mana keberhasilan parsial dapat diterima.

Job Chaining untuk Alur Kerja Berurutan

Jika batching menangani beban kerja paralel, chaining menjamin eksekusi berurutan. Setiap job dalam rantai hanya berjalan setelah job sebelumnya berhasil. Jika ada job yang gagal, sisa rantai dibatalkan dan callback catch dijalankan.

App/Services/OrderWorkflow.phpphp
use App\Jobs\ValidatePayment;
use App\Jobs\ReserveInventory;
use App\Jobs\SendConfirmation;
use App\Jobs\GenerateShippingLabel;
use Illuminate\Support\Facades\Bus;

public function processOrder(Order $order): void
{
    // Each job runs only after the previous one succeeds
    Bus::chain([
        new ValidatePayment($order),
        new ReserveInventory($order),
        new GenerateShippingLabel($order),
        new SendConfirmation($order),
    ])
    ->onQueue('orders')
    ->catch(function (\Throwable $e) use ($order) {
        // Roll back the order if any step fails
        $order->update(['status' => 'failed']);
        logger()->error('Order chain failed', [
            'order_id' => $order->id,
            'step' => $e->getMessage(),
        ]);
    })
    ->dispatch();
}

Chaining unggul untuk alur kerja domain di mana urutan langkah penting — pembayaran harus tervalidasi sebelum inventory direservasi, dan label pengiriman bergantung pada inventory yang dikonfirmasi.

Siap menguasai wawancara Laravel Anda?

Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.

Queue Middleware untuk Kebutuhan Lintas Sektor

Queue middleware membungkus eksekusi job dengan logika yang dapat digunakan ulang: rate limiting, deduplikasi, atau circuit breaker. Daripada menanamkan kebutuhan ini di setiap job, middleware menjaga job tetap fokus pada logika bisnis.

App/Jobs/Middleware/RateLimitedJob.phpphp
namespace App\Jobs\Middleware;

use Closure;
use Illuminate\Support\Facades\RateLimiter;

class RateLimitedJob
{
    public function __construct(
        private string $key,
        private int $maxAttempts = 10,
        private int $decaySeconds = 60
    ) {}

    public function handle(object $job, Closure $next): void
    {
        // Release job back to queue if rate limit exceeded
        if (RateLimiter::tooManyAttempts($this->key, $this->maxAttempts)) {
            $job->release($this->decaySeconds);
            return;
        }

        RateLimiter::hit($this->key, $this->decaySeconds);

        $next($job);
    }
}

Middleware diterapkan dengan mendefinisikan method middleware() pada class job:

App/Jobs/CallExternalApi.phpphp
public function middleware(): array
{
    return [
        new RateLimitedJob(
            key: 'external-api',
            maxAttempts: 30,
            decaySeconds: 60
        ),
        // Prevent duplicate jobs from running concurrently
        (new WithoutOverlapping($this->apiResource->id))
            ->releaseAfter(300)
            ->expireAfter(600),
    ];
}

Middleware WithoutOverlapping menggunakan lock atomik untuk memastikan hanya satu instance job (diidentifikasi dengan key) yang berjalan dalam satu waktu. Dikombinasikan dengan rate limiting, ini mencegah pemrosesan duplikat sekaligus throttling API.

Penanganan Job Gagal dan Strategi Retry

Sistem queue produksi membutuhkan penanganan kegagalan yang kuat. Laravel menyimpan job yang gagal dalam tabel failed_jobs dengan payload lengkap, exception trace, dan queue/connection yang menghasilkan kegagalan. Method failed() pada setiap class job dijalankan setelah seluruh percobaan retry habis.

Mengkonfigurasi perilaku retry per job memberikan kontrol yang lebih halus:

App/Jobs/SyncExternalData.phpphp
class SyncExternalData implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 5;

    // Exponential backoff: 10s, 30s, 60s, 120s, 300s
    public function backoff(): array
    {
        return [10, 30, 60, 120, 300];
    }

    // Job-specific timeout
    public int $timeout = 180;

    // Maximum exceptions before marking as failed
    public int $maxExceptions = 3;

    public function retryUntil(): \DateTime
    {
        // Keep retrying for up to 24 hours
        return now()->addHours(24);
    }

    public function handle(): void
    {
        $response = Http::timeout(30)
            ->retry(2, 1000)
            ->get('https://api.vendor.com/data');

        if ($response->failed()) {
            // Release back to queue with delay for transient failures
            $this->release(60);
            return;
        }

        DataSync::process($response->json());
    }

    public function failed(\Throwable $e): void
    {
        Notification::route('slack', config('services.slack.ops_channel'))
            ->notify(new SyncFailed($e));
    }
}

Perbedaan antara $tries, $maxExceptions, dan retryUntil() penting dalam wawancara. $tries menghitung setiap percobaan termasuk release manual. $maxExceptions hanya menghitung exception yang tidak tertangani. retryUntil() menetapkan jendela waktu terlepas dari jumlah percobaan.

Manajemen Queue Worker dan Deployment

Queue worker di produksi membutuhkan supervisi proses, restart yang halus saat deployment, dan manajemen sumber daya. Supervisor adalah tool standar untuk menjaga worker tetap hidup.

ini
; /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
stopwaitsecs=3600
user=www-data
numprocs=4
redirect_stderr=true
stdout_logfile=/var/log/worker.log
stopasgroup=true
killasgroup=true

Pertimbangan deployment utama:

  • Restart halus: php artisan queue:restart memberi sinyal ke worker untuk menyelesaikan job saat ini sebelum restart. Ini mencegah korupsi job selama deploy.
  • Maks waktu/job: --max-time=3600 dan --max-jobs=1000 mencegah memory leak dengan mendaur ulang proses worker secara periodik.
  • Interval sleep: --sleep=3 mengontrol berapa lama worker menunggu sebelum melakukan polling queue kosong lagi. Nilai lebih rendah meningkatkan responsivitas tetapi juga beban database/Redis.
  • Beberapa queue: --queue=critical,default,low memproses queue berdasarkan urutan prioritas. Worker mengosongkan queue critical sebelum menyentuh default.

Laravel 12.37 memperkenalkan connection queue background yang menunda job menggunakan Concurrently::defer(). Driver ini melakukan serialisasi dan menjalankan job dalam proses PHP terpisah — berguna untuk job ringan yang tidak memerlukan infrastruktur queue penuh.

Unique Job dan Payload Terenkripsi

Dua pola yang sering muncul di produksi maupun diskusi wawancara: memastikan job hanya berjalan satu kali untuk key tertentu, dan melindungi data sensitif dalam payload job.

App/Jobs/RebuildSearchIndex.phpphp
use Illuminate\Contracts\Queue\ShouldBeUnique;

class RebuildSearchIndex implements ShouldQueue, ShouldBeUnique
{
    // Lock duration in seconds
    public int $uniqueFor = 3600;

    public function __construct(
        public readonly string $indexName
    ) {}

    // Unique key scopes the lock to this specific index
    public function uniqueId(): string
    {
        return $this->indexName;
    }

    public function handle(): void
    {
        SearchIndex::rebuild($this->indexName);
    }
}

Untuk job yang membawa data sensitif (kredensial pengguna, token pembayaran), interface ShouldBeEncrypted mengenkripsi seluruh payload terserialisasi saat tersimpan:

App/Jobs/ProcessPayment.phpphp
use Illuminate\Contracts\Queue\ShouldBeEncrypted;

class ProcessPayment implements ShouldQueue, ShouldBeEncrypted
{
    public function __construct(
        private string $paymentToken,
        private float $amount
    ) {}

    public function handle(PaymentGateway $gateway): void
    {
        $gateway->charge($this->paymentToken, $this->amount);
    }
}

Payload dienkripsi dengan application key sebelum disimpan di Redis atau database. Worker mendekripsinya secara otomatis sebelum deserialisasi.

Pertanyaan Wawancara Umum tentang Queue Laravel

Wawancara teknis sering menguji pemahaman arsitektur queue di luar pengetahuan API permukaan.

Apa yang terjadi ketika job dalam queue mereferensikan model Eloquent yang sudah dihapus? Dengan SerializesModels, worker mencoba mengambil model berdasarkan ID saat memproses job. Jika model sudah tidak ada, Laravel melempar ModelNotFoundException. Untuk menangani ini dengan baik, atur properti $deleteWhenMissingModels ke true — job akan dihapus dari queue secara senyap alih-alih gagal.

Apa beda ShouldBeUnique dengan middleware WithoutOverlapping? ShouldBeUnique mencegah job di-dispatch jika ada job lain dengan unique key yang sama sudah berada di queue. WithoutOverlapping mengizinkan dispatch tetapi mencegah eksekusi bersamaan — jika job dengan key yang sama sedang berjalan, instance baru dilepas kembali ke queue. Keduanya memecahkan masalah berbeda dan dapat dikombinasikan.

Kapan retryUntil() lebih dipilih daripada $tries? retryUntil() cocok untuk job yang berinteraksi dengan layanan eksternal di mana waktu pemulihan tidak dapat diprediksi. Jumlah retry tetap ($tries = 3) bisa habis selama gangguan singkat. retryUntil() menetapkan jendela waktu (misalnya 24 jam) dan terus mencoba ulang dengan backoff hingga layanan pulih atau jendela berakhir.

Bagaimana queue priority bekerja dengan banyak queue? Menjalankan queue:work --queue=critical,default,low membentuk sistem prioritas. Worker mengosongkan queue critical sepenuhnya sebelum memeriksa default, dan default sebelum low. Artinya, job berprioritas rendah dapat kelaparan saat beban puncak. Untuk SLA ketat, worker khusus per queue memberi jaminan lebih baik.

Mulai berlatih!

Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.

Kesimpulan

  • Queue Laravel mengabstraksi backend queue di balik API independen-driver yang mendukung Redis, SQS, database, dan connection background baru di Laravel 12.37
  • Job batching menangani beban kerja paralel dengan pelacakan progres kolektif, sementara chaining menerapkan eksekusi berurutan untuk alur kerja domain
  • Queue middleware (rate limiting, WithoutOverlapping) menjaga kebutuhan lintas sektor tetap di luar logika bisnis job
  • Penanganan job gagal menggabungkan $tries, $maxExceptions, retryUntil(), dan exponential backoff untuk strategi retry yang tangguh
  • ShouldBeUnique mencegah dispatch duplikat; ShouldBeEncrypted melindungi payload sensitif saat tersimpan
  • Worker produksi membutuhkan Supervisor, restart halus selama deploy, dan manajemen memori melalui --max-time dan --max-jobs
  • Persiapan wawancara perlu mencakup perbedaan antara unik saat dispatch dengan locking saat eksekusi, perilaku serialisasi model, serta starvation queue prioritas

Untuk berlatih pertanyaan wawancara Laravel, bank soal SharpSkill mencakup queue, middleware, dan pola Eloquent dengan penjelasan detail.

Mulai berlatih!

Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.

Tag

#laravel
#queues
#jobs
#php
#async
#architecture

Bagikan

Artikel terkait