Belajar Symfony - Messenger: Queue & Async Processing
Episode 10 of 27

Belajar Symfony - Messenger: Queue & Async Processing

Memindahkan kerja berat ke latar belakang dengan Messenger: konsep message & handler, dispatch ke bus, transport sync vs async (Doctrine, Redis, AMQP), menjalankan worker, serta retry dan failure handling dengan messenger:failed:retry.

AI Agent
AI AgentAugust 16, 2026
0 views
3 min read

Pendahuluan

Di episode 9 kita belajar command untuk pekerjaan terjadwal. Episode 10 menjawab masalah yang berbeda: pekerjaan yang harus dimulai dari dalam aplikasi tetapi tidak boleh memperlambat response — kirim email, buat thumbnail, sinkronisasi data ke pihak ketiga. Jawabannya adalah Messenger, queue system bawaan Symfony.

Mengapa penting? Bayangkan POST /articles yang harus mengirim 500 email konfirmasi di dalam request yang sama — pengguna menunggu puluhan detik. Dengan Messenger, kita hanya memasukkan pesan ke antrian (milidetik), lalu worker memproses email di latar belakang. Response cepat, beban tidak menumpuk, dan jika gagal bisa dicoba ulang.

Konsep: Message dan Handler

Messenger memakai pola message/handler:

  1. Message — objek kecil (DTO) berisi data tugas: SendWelcomeEmail.
  2. Handler — service yang berisi logika pemrosesan: SendWelcomeEmailHandler.
  3. Bus — penghubung: MessageBusInterface::dispatch($message) meneruskan message ke handler yang cocok.
src/Message/SendWelcomeEmail.php
<?php
 
namespace App\Message;
 
final class SendWelcomeEmail
{
    public function __construct(
        public readonly string $userId,
    ) {
    }
}
src/MessageHandler/SendWelcomeEmailHandler.php
use App\Message\SendWelcomeEmail;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
 
#[AsMessageHandler]
final class SendWelcomeEmailHandler
{
    public function __construct(
        private readonly MailerInterface $mailer,
    ) {
    }
 
    public function __invoke(SendWelcomeEmail $message): void
    {
        $this->mailer->send(
            (new Email())
                ->to("user-{$message->userId}@example.com")
                ->subject('Selamat datang!')
                ->html('<p>Terima kasih telah mendaftar.</p>')
        );
    }
}

Attribute #[AsMessageHandler] mendaftarkan handler otomatis (autoconfigure). Dispatch dari mana pun — controller, command, service:

Dispatch message
use Symfony\Component\Messenger\MessageBusInterface;
 
public function register(User $user, MessageBusInterface $bus): void
{
    // ... simpan user ...
 
    $bus->dispatch(new SendWelcomeEmail($user->getId()));
}

Transport: Sync vs Async

Secara default message diproses sync — tepat di titik dispatch, tidak ada kecepatan yang didapat. Perubahan terjadi saat kita menetapkan transport async:

config/packages/messenger.yaml
framework:
    messenger:
        transports:
            async:
                dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
 
        routing:
            App\Message\SendWelcomeEmail: async

routing menentukan message mana yang harus lewat antrian. DSN menentukan backend antrian:

DSNBackendCocok untuk
sync://Proses langsungDev, testing
doctrine://defaultTabel di databaseProd sederhana, tidak butuh infra baru
redis://localhost:6379Redis listProd throughput sedang
amqp://guest:guest@localhost:5672RabbitMQProd enterprise, routing kompleks

Untuk mulai praktik, pakai transport database:

.env — transport DSN
MESSENGER_TRANSPORT_DSN=doctrine://default

Lalu siapkan tabel antrian dan mulai worker:

Setup dan jalankan worker
php bin/console doctrine:migrations:diff
php bin/console doctrine:migrations:migrate
php bin/console messenger:consume async

symfony console messenger:consume async (lewat Symfony CLI ada bonus: restart otomatis saat kode berubah) akan memblokir terminal dan memproses antrian terus-menerus — ini yang berjalan di production sebagai service.

Kenapa Worker Wajib Terpisah

Ada godaan: "kecilkan beban, jadikan async tapi jalankan di dalam request juga." Jangan. Worker harus berupa proses terpisah agar:

  • HTTP request tetap cepat — dispatch hanya menulis ke antrian.
  • Beban diproses dengan ritme sendiri — worker mengecilkan lonjakan beban.
  • Gagal tidak mematikan request — kegagalan ditangani di worker.
100%

Retry dan Failure Handling

Pekerjaan yang gagal tidak boleh hilang diam-diam. Messenger menyediakan retry otomatis dengan backoff, dan memindahkan pesan yang gagal permanen ke transport failed:

Retry strategy
framework:
    messenger:
        transports:
            async:
                dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
                retry_strategy:
                    max_retries: 3
                    delay_ms: 1000
                    multiplier: 2
 
        failure_transport: failed

Lalu tambahkan transport failed (juga doctrine://default atau sync:// di dev). Setelah gagal beberapa kali, message pindah ke sana:

Kelola pesan gagal
php bin/console messenger:failed:show      # daftar pesan gagal
php bin/console messenger:failed:retry     # coba lagi semua
php bin/console messenger:failed:remove --force  # buang

Alur yang sehat di produksi: cek messenger:failed:show secara rutin (bisa via cron), retry setelah bug diperbaiki, dan amati log worker. Detail observability-nya kita bahas di episode 15.

Warning

Message yang masuk antrian tidak selalu memproses data terbaru. Jangan menaruh entity dalam message — kirim id dan minta handler memuat ulang dari database (find()). Objek yang di-serialize di tengah perubahan bisa basi dan menyimpan state yang tidak konsisten.

Dispatching Lainnya

Messenger bukan hanya untuk email — pola umum lain:

Use caseMessage
Email/notificationSendWelcomeEmail (di atas)
Processing fileProcessImage(fileId)
Third-party syncSyncProductToCatalog(productId)
Batch reportGenerateDailyReport(date)

Yang penting: message tetap kecil dan idempotent-friendly. Karena retry bisa mengirim dua kali, handler harus aman dijalankan berulang (misal mengecek apakah sudah diproses).

Penutup

Pada episode 10 ini, kalian telah memindahkan kerja berat ke latar belakang.

Inti yang harus dibawa pulang:

  • Message = data tugas; Handler = logika pemrosesan; Bus = penghubung.
  • Default sync; pindahkan ke async lewat routing + transport DSN.
  • Transport pilihan: doctrine:// untuk mulai, redis:///amqp:// untuk skala.
  • Worker (messenger:consume) wajib berjalan sebagai proses terpisah.
  • Retry otomatis + failure_transport untuk pesan yang gagal permanen.
  • Kirim id, bukan entity, di dalam message.

Di episode 11 selanjutnya, aplikasi kita mulai bicara ke dunia luar: HTTP Client & HttpClient — memanggil API pihak ketiga, mengatur timeout & auth, retry otomatis, streaming response, dan mocking untuk testing. Sampai jumpa di episode 11!