symfony/beanstalkd-messenger
Symfony Messenger transport for Beanstalkd. Send and consume messages via Beanstalkd queues using a Messenger-compatible transport with DSN configuration and worker support, ideal for lightweight background job processing in Symfony apps.
First, decide if you actually need it. Laravel already ships a Beanstalkd queue driver ('beanstalkd' => ['driver' => 'beanstalkd', ...] in config/queue.php) built on pda/pheanstalk. If your goal is "push a job, run php artisan queue:work", stop — this package will not help you and will not plug into Illuminate\Queue at all.
symfony/beanstalkd-messenger is a Symfony Messenger transport bridge. It implements Symfony\Component\Messenger\Transport\SenderInterface / ReceiverInterface on top of Beanstalkd (via Pheanstalk). It is useful in a Laravel codebase only when:
DelayStamp, custom middleware),Minimal install
composer require symfony/messenger symfony/beanstalkd-messenger pda/pheanstalk
Where to look first
Symfony\Component\Messenger\Bridge\Beanstalkd\Transport\BeanstalkdTransportFactory::createTransport() — the authoritative list of DSN query options for the version you installed (tube, timeout, ttr, serializer, etc.). Don't trust blog posts over this method.BeanstalkdReceiver / BeanstalkdSender — a few hundred lines; read both to understand ack/nack/release behaviour before you build retry logic on top.Symfony\Component\Messenger\Transport\Transport::fromDsn() — the factory entry point you'll call from a service provider.First use case: a tiny DTO on a tube
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Transport\Transport;
$transport = Transport::fromDsn('beanstalkd://127.0.0.1:11300?tube=emails&ttr=120');
$transport->send(new Envelope(new SendWelcomeEmail($userId)));
// Consume (no artisan command exists for this — you write it)
$worker = new \Symfony\Component\Messenger\Worker(
[$transport],
$bus, // Symfony MessageBusInterface
$logger,
$eventDispatcher,
$retryStrategy
);
$worker->run(['time_limit' => 3600, 'memory_limit' => '256M']);
Register the transport, bus, and handlers map as singletons in an AppServiceProvider (or a dedicated MessengerServiceProvider). Laravel's container can hold MessageBusInterface perfectly fine; dispatch() and ShouldQueue will not, however, route to it.
Tube-per-concern, not tube-per-app. Beanstalkd tube names are global on the server. In multi-environment or multi-tenant setups, prefix them (prod-emails, tenant-42-billing) or you'll have staging workers chewing on production jobs. Wire routing explicitly:
$bus = new MessageBus([
new SendMessageMiddleware(new SendersLocator([
SendWelcomeEmail::class => ['beanstalkd_emails'],
SyncInvoice::class => ['beanstalkd_billing'],
], $container)),
new HandleMessageMiddleware(new HandlersLocator([
SendWelcomeEmail::class => [new SendWelcomeEmailHandler()],
])),
]);
Send IDs, not models. Laravel devs reach for SerializesModels reflexively. Messenger's default PhpSerializer will happily serialize an Eloquent model — and then explode on the consumer when the schema or class changes. Send a small DTO with scalar IDs and reload inside the handler. This also keeps you under Beanstalkd's job-size cap (see Gotchas).
Replace Laravel job features deliberately. You give up ShouldQueue, tries, backoff, retryUntil, failed(), middleware(), and Horizon visibility. Rebuild them with Messenger primitives:
| Laravel concept | Messenger equivalent |
|---|---|
tries / backoff |
RetryStrategy + SendFailedMessageForRetryListener |
failed_jobs table / failed() |
failure transport (a second Beanstalkd tube or Doctrine transport) + FailedMessageEvent listener |
| job middleware | bus middleware (MiddlewareInterface) |
delay() |
DelayStamp (maps to Beanstalkd put delay) |
queue:work + Supervisor |
custom artisan messenger:consume command + Supervisor |
| chunked batching | BatchHandler / Bus batching, if you're on Symfony 7.x |
Run workers as an artisan command. There is no messenger:consume shipped for Laravel (that command lives in symfony/framework-bundle). Write one that builds the Worker, handles SIGTERM (call $worker->stop()), and exits on time_limit/memory_limit. Let Supervisor restart it. Mirror the flags Laravel devs expect (--time-limit, --memory, --tries) so ops doesn't need a new runbook.
Coexistence. You can run Laravel's beanstalkd driver for ordinary jobs and this bridge for the event bus, as long as the tubes never overlap. Use one Supervisor program per tube so a stuck billing consumer doesn't starve emails.
Testing. Swap in in-memory:// or sync:// for unit tests; for integration, send to a real tube and assert with $transport->get() (returns an iterable of envelopes). Pheanstalk's stats-tube is your friend when a test hangs.
ttr seconds. If a handler runs longer than TTR, the job is redelivered while still running, giving you duplicate execution. Set ttr well above worst-case handler time, or touch() the job from the handler periodically (Pheanstalk's touch() via a decorated receiver).-z server flag controls max job size; the stock value is 65535 bytes. A serialized Eloquent model with relations blows past this instantly. Keep payloads as IDs + minimal scalars.RetryStrategy is configured, the receiver's reject/release behaviour can cycle indefinitely with no failed_jobs row to inspect. Always pair the receiver with a retry strategy and a failure transport, and alert on buried-job count.PhpSerializer requires identical class names and shapes on both ends; JSON serializer drops non-serializable stamps. Pick one explicitly, version your message contracts, and never change a DTO's property names without a rollout plan.queue.connections.beanstalkd.queue prefix. ?tube=default in staging and production on one shared server is a real incident waiting to happen.BeanstalkdTransportFactory::createTransport() in vendor/ rather than trusting docs; also URL-encode tube names with colons or dashes.composer.json, and keep a thin wrapper interface around the transport so you can swap to symfony/amqp-messenger or Laravel's native queue without rewriting call sites.pheanstalk (or telnet 11300) → list-tubes, stats-tube <name>, peek-ready, peek-buried, kick 10. Buried jobs are this stack's equivalent of failed_jobs, so build a small artisan beanstalkd:inspect command early.TransportInterface for tracing/metrics, implement a custom TransportFactory if you need Beanstalkd priority (Messenger has no PriorityStamp, so you'll need one), and inject a custom SerializerInterface on the transport to control the wire format.How can I help you explore Laravel packages today?