Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Symfony Nats Messenger Laravel Package

idct/symfony-nats-messenger

A Symfony Messenger transport for NATS that lets PHP applications publish and consume messages over NATS subjects with native Symfony integration, configuration, and worker support for event-driven architectures.

View on GitHub
Deep Wiki
Context7

Technical Evaluation

Architecture fit. This is a Symfony Messenger transport, not a Laravel queue driver. It implements TransportInterface, MessageCountAwareInterface and SetupableTransportInterface, and ships IDCTNatsMessengerBundle for Symfony Flex auto-registration. In a Laravel application that bundle is inert — Flex does not exist, so the transport factory and the igbinary serializer must be registered by hand in a service provider, and a MessageBus (with SendersLocator and HandlersLocator) must be constructed outside Laravel's container conventions. The net architectural position is unchanged by v5.7.0: we would be running two queue subsystems side by side (Illuminate\Queue for Laravel jobs, Symfony Messenger for NATS traffic) with no shared retry, failure, or observability model. That is a deliberate, defensible choice only if NATS JetStream is already the cross-service backbone and PHP is a participant rather than the owner.

Integration feasibility. Technically feasible, moderate-to-high effort. The hard parts are not the transport — DSN parsing (nats-jetstream://, nats-jetstream+tls://), TLS/mTLS, JWT/NKey auth, stream and consumer provisioning, retention and replica settings, batching, DelayStamp scheduling and Nats-Msg-Id deduplication are all implemented and, per the repo, covered by Behat functional scenarios against a real NATS server. The hard parts are the seams:

  • Worker lifecycle. There is still no queue:work equivalent. We must write and supervise an artisan command that owns the Messenger Worker, handles --keepalive, graceful termination, memory limits and restart. Laravel provides none of this for free — no Horizon, no failed_jobs table, no queue:retry. v5.7.0 materially improves this seam: with reconnect: true a worker now polls through a broker outage shorter than one recovery cycle instead of throwing and exiting, and a pull the server ended for its own shutdown or a leadership change returns an empty batch rather than a missing-consumer error. That converts a common class of broker-restart incidents from "supervisor restarts the worker" into "the worker waits." It does not, however, remove the need to write the command.
  • Serializer coupling. Unchanged and still the largest interop gap. The default IgbinarySerializer falls back to PhpSerializer, and both wrap a Symfony Envelope. That wire format is PHP-shaped and not consumable by a Go/Node service without replicating the envelope contract. If the reason we want NATS is polyglot messaging, this requires a custom serializer extending AbstractEnveloperSerializer plus a matching contract on every non-PHP consumer.
  • Concurrency model. The underlying idct/php-nats-jetstream-client is amphp-coroutine based. Running that inside a Laravel worker is one thing; running it under Octane (Swoole/RoadRunner), which already owns an event loop, is a different risk class entirely and should be treated as unsupported until proven. The reconnect mode increases the amount of long-lived loop state and reconnect bookkeeping inside the client, which if anything raises rather than lowers the bar on proving Octane coexistence.
  • Observability. New in v5.7.0: the transport factory accepts an optional PSR-3 logger and the transport a fourth constructor argument, and psr/log ^3.0 is now a direct requirement. In Symfony the bundle injects the logger service automatically; in Laravel we do this by hand in our service provider — which is straightforward, but means the log channel, format and level are our configuration responsibility, not the package's.

Technical risk. Concentrated in maturity and ownership, not in code quality — and the quality signal just strengthened. v5.7.0 reports 669 unit tests, 99.78% statement coverage, and 1008 of 1040 mutants killed (96.9%) with the 32 survivors individually documented, plus reconnect tests that exercise the real client against a scripted connection that can go silent, fail a write, close server-side, hold or refuse dials, and Behat scenarios that restart a real NATS server with docker under a running worker and a running sender. That is unusually rigorous for a package of this size and it is exactly the kind of test surface that makes a fork survivable. Adoption, however, is still the concern — small star count, no known dependents, single primary maintainer. The last release: 2026-10-17 timestamp from the prior assessment remains anomalous relative to a current review and should be re-verified from Packagist. New in this release: a second contributor (Petar Fedorovsky) authored the original reconnect work, and the maintainer redesigned it as a mode across two follow-up PRs — a mild positive on bus factor, not a resolution of it.

Key questions.

  1. Is NATS JetStream already a company-standard backbone, or would this package be the reason to adopt it? If the latter, the calculus changes entirely.
  2. Do any non-PHP services need to consume these messages? If yes, who owns the JSON envelope contract and its versioning?
  3. Will any of this run under Octane? If yes, we need a written statement from the maintainer or a spike proving the amphp loop coexists — reconnect mode makes this question more important, not less.
  4. What is our poison-message policy — Symfony TERM with app-side retry limits, or NAK with server-side max_deliver? Who sets and audits max_deliver?
  5. New: What is our reconnect policy per workload class — reconnect: true with how many max_reconnect_attempts, and does that budget fit inside our supervisor and alerting tolerances? (Client default of 10 attempts ≈ 109 s against a refusing server.)
  6. New: Do we accept that a reconnect that gives up discards buffered acknowledgements, so those messages are redelivered after ack_wait — i.e. that an outage can produce a duplicate burst on recovery?
  7. Are we prepared to fork and maintain this package if the maintainer stops? Who is the named owner?
  8. Does the business accept that NATS-delivered messages will not appear in Horizon, failed_jobs, or our existing queue dashboards?

Integration Approach

Stack fit. Laravel 11/12 and this package share PHP 8.2+, and Laravel already depends on several Symfony components (console, process, http-foundation) — but not symfony/messenger. There is no version conflict, and equally no free integration. Two deltas from v5.7.0 to verify in the spike:

  • psr/log ^3.0 is now a direct requirement. Confirm the target Laravel version resolves to psr/log 3.x before pinning; a Laravel 10 holdout would take a dependency conflict where previously there was none.
  • The factory's optional PSR-3 logger means our service-provider wiring should pass Laravel's logger explicitly and route it to a dedicated Monolog channel (the upstream bundle uses messenger). We should not inherit the default channel — connection lifecycle events are logged at info on every reconnect and errors at error, and both levels need deliberate routing or they will either spam or hide.

Recommendation is unchanged: scope it as "NATS integration," not "a composer package we installed."

Migration path. Do not migrate existing Laravel queues. Run this as an additive, event-oriented path: NATS JetStream carries cross-service domain events and integration messages; Illuminate\Queue keeps owning user-facing, latency-sensitive, retry-heavy jobs. This keeps blast radius small, keeps Horizon as the operational source of truth for the things the business already depends on, and lets us evaluate the transport on lower-stakes traffic first.

Compatibility.

  • Requires a NATS 2.x server with JetStream enabled; stream creation, subject merging and config preservation are handled by the transport, which means the app has infrastructure-mutating privileges. Prefer pre-provisioning streams out-of-band and treating the app as a client only. Note that with auto_setup + reconnect, provisioning is now re-verified after a reconnect, and a send landing on a server without the stream gets one setup() and one republish — this is an improvement, but it also means the app will attempt to mutate infrastructure mid-incident. One more argument for pre-provisioning.
  • igbinary is optional; the PhpSerializer fallback keeps the install simple but hard-codes the PHP-only wire format. Decide this early — it is a one-way door for interop.
  • TLS/mTLS and JWT/NKey are supported, so it can meet enterprise auth requirements without a sidecar.
  • Laravel's own broadcast, events and notifications do not route through Messenger; any bridging is custom code we own.
  • Upgrade from 5.6 is a no-op — the option is off by default and prior behaviour is preserved, with two always-on changes: 409 Server Shutdown / 409 Leadership Change on a pull now yields an empty batch, and logger injection occurs when a logger service is present. Cheap upgrade, but our alerting must not treat the new empty-batch case as an anomaly.

Sequencing.

  1. Week 1 — viability spike (throwaway). Boot symfony/messenger inside a Laravel service provider, register NatsTransportFactory manually with an injected PSR-3 logger, publish and consume one message against a local NATS JetStream container. Gate: works without the Symfony bundle, no container conflicts, no psr/log resolution conflict.
  2. Week 1–2 — interop decision. Prototype a JSON serializer against AbstractEnveloperSerializer and confirm a non-PHP consumer can read it. Gate: written wire-format contract, or an explicit decision that this is PHP-only.
  3. Week 2–3 — worker command. Build nats:consume with keepalive, memory ceiling, graceful shutdown, structured logging on the dedicated channel, and a health signal. Include reconnect: true with a deliberately chosen max_reconnect_attempts. Gate: survives kill -9 and a docker stop of the broker of at least the length of one recovery cycle, without message loss beyond documented at-least-once semantics, and without the worker exiting.
  4. Week 3–4 — Octane and failure drills. NAK/TERM behaviour under induced failure; poison-message containment; connection-loss recovery (ping_after_idle, connection_timeout, request_timeout tuning); the interaction between max_reconnect_attempts and max_batch_timeout; and a forced reconnect-give-up to observe the discarded-acknowledgement redelivery burst.
  5. Week 4+ — pilot. One non-critical integration event, one team, two sprints of production observation before any expansion.

Exit criteria for abandoning the effort: Octane incompatibility with no workaround, serialization interop proving infeasible within the spike, or maintainer unresponsiveness on a blocking defect.

Operational Impact

Maintenance. Plan for ownership, not consumption. Pin the version exactly, vendor the lockfile, mirror the repo internally, and designate a named engineer accountable for upgrades against Symfony 8 and PHP 8.5. Budget modest ongoing effort for tracking upstream releases and one fork-ready contingency. v5.7.0 improves the fork calculus: the codebase is small, the test suite is extensive and mutation-tested with survivors individually justified, and the reconnect work is documented at the granularity of per-operation timeout bounds — all of which shorten the time for a competent engineer to take over. MIT license permits the fork. This remains the main mitigant to the bus-factor risk.

Support. No commercial support. Upstream responsiveness is unproven at this adoption level, though the two-PR redesign with an outside contributor is a better signal than the prior assessment could claim. Internal runbooks must still be written by us: how to drain a consumer, how to replay a subject, how to inspect a stalled stream, how to recover from a wedged amphp loop, and — new — how to interpret and tune the reconnect state machine. Treat "we can debug this ourselves" as a staffing prerequisite, not an aspiration.

Scaling. Horizontal scale is worker-count based, with consumer strategy dictating semantics: shared consumers (batching=1) for competing-consumer throughput, independent consumers where each worker needs the full stream. batching and max_batch_timeout are the throughput/latency dial; stream_replicas provides broker-side HA. Ordering is not guaranteed across multiple consumers on a subjects — any workflow that silently assumes ordered delivery is a latent defect. Stream retention (stream_max_age, stream_max_bytes, stream_max_messages) is a capacity-planning input that must be set deliberately, not left at defaults.

New capacity consideration from v5.7.0: a reconnecting worker is occupied, not free. During recovery it holds its worker slot and its consumer assignment while it waits within its own budget, so a broker outage parks the entire worker pool for the duration of one recovery cycle — roughly 109 s with the client default of 10 attempts against a refusing server, and proportionally longer if we raise max_reconnect_attempts to ride out maintenance windows. That is the right trade for most workloads, but it must be an explicit sizing decision: longer tolerance means fewer workers are doing useful work during an outage, and it means our liveness alerting must distinguish "polling through an outage" from "wedged."

Failure modes to plan for.

  • Poison-message loops. retry_handler: nats (NAK) delegates redelivery to the server and depends on max_deliver being set correctly; symfony (TERM) relies on our own retry strategy. Either way, an unset or misconfigured limit yields infinite redelivery on a single bad message. Explicit policy and a monitored counter required.
  • Duplicate bursts after a give-up reconnect. New in v5.7.0 and the most important operational nuance of the release: a reconnect that gives up discards the acknowledgements it had buffered and logs that fact at error level; those messages come back after ack_wait. So an outage can be followed by a redelivery wave that is entirely expected behaviour. Handlers must be idempotent, and the runbook must say so before someone diagnoses it as a bug. This reinforces at-least-once, never exactly-once.
  • ack_sync does not ride out an outage. Synchronous acknowledgement that hits the outage still fails after request_timeout regardless of reconnect mode. Any handler using ack_sync needs its own failure path and must not assume the reconnect option covers it.
  • Deduplication window. The deduplicate option and DeduplicationIdStamp are bounded by the server's duplicate window; retries after the window — including post-outage redelivery — are not deduplicated.
  • Connection and event-loop stalls. Reconnection is handled (ping_after_idle, dial/request timeouts), but a stalled coroutine loop presents as a silent consumer. With reconnect on, the decision logic on a failed PING (recovering / already recovered / open-with-no-reconnect / gave up) is subtle enough that liveness monitoring must key on message age, not process-up or connection status. This is non-negotiable.
  • Fire-and-forget write stalls. Upstream acknowledges a known client issue (ideaconnect/php-nats-jetstream-client#244) where a fire-and-forget acknowledgement whose write fails while the client still reports the connection open can wait for a whole attempt loop. Watch for latency spikes correlated with flaky networking rather than broker restarts.
  • Scheduled message drift. DelayStamp maps to NATS schedule headers; clock and server-version behaviour should be validated in the pilot.
  • Serialization breakage on upgrade. Any change to the envelope or serializer is a cross-version compatibility event and needs a migration plan, unlike a Laravel queue payload change confined to one app.

Ramp-up. Moderate. The configuration surface is wide — now slightly wider, with reconnect and max_reconnect_attempts added — and each option has a production consequence: retention, storage, replication, consumers, batching, retry, dedup, TLS, three separate timeouts, and now the reconnect budget, whose correct value is a function of the other timeouts (request_timeout for requests, max_batch_timeout plus one second for pulls) and of our supervisor and alerting tolerances. Budget a week of focused enablement for the two engineers who will own it, and require them to read the configuration builder source and the README's Automatic Reconnect table rather than relying on summaries, since option semantics are the real API here. Do not spread this across a broad team on the assumption it behaves like a Laravel queue driver; it does not, and the failure modes are quieter.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource