harris21/laravel-fuse
Laravel Fuse adds fuzzy search to Eloquent models and collections, helping users find results despite typos. Configure searchable fields and thresholds, then run simple fluent queries for fast, relevant matches without external search services.
Architecture fit
harris21/laravel-fuse is a circuit-breaker for Laravel that sits on the queue path, not the HTTP path: middleware gates whether a job may run, a cache-backed state machine tracks per-service health, probes (OldestJobProbe and equivalents) decide when to close an open circuit, and transition listeners plus a /fuse status page expose state. That is a good architectural fit for any Laravel estate where queue workers make outbound calls to flaky third parties (payments, KYC, shipping, email, partner APIs) and where $tries/backoff alone is too blunt — the value is stopping a queue full of retries from hammering a degraded dependency and from duplicating side effects.
The package is deliberately thin: middleware, a config file, a cache namespace, a Blade view. No database migrations, no service provider gymnastics beyond registration, no long-running daemon. That keeps it composable with Horizon, dispatchSync, and multi-queue topologies. The main architectural constraint is that state lives in the cache, so the cache is now on the critical path for job dispatch correctness — which is precisely what v0.11.0 is about.
Integration feasibility
Straightforward for a team already on Laravel queues: publish config and (optionally) views, register middleware on the relevant jobs or job base class, define per-service thresholds, register listeners, gate the status page behind existing auth middleware. Days, not weeks, for a first service.
v0.11.0 materially improves feasibility because it fixes a class of silent-misconfiguration bugs that would otherwise have burned us during rollout:
payments.stripe) previously fell through to global defaults with no warning. Any naming convention using dots was silently unconfigured in production.threshold no longer yields a hard-coded 50, or 60 during 09:00–17:59 through a peak window nobody configured. Configs now resolve predictably, and peak windows can cross midnight (22 → 6), which matters for teams with offshore traffic.Feasibility drops for anyone who built a custom recovery strategy that throws deliberately. That was previously a supported way to hard-stop a job; it now releases the job (except on the sync driver, where the error is still thrown). That pattern must be audited before upgrade.
Technical risk
Moderate, and mostly migration risk rather than runtime risk. The release is a net risk reduction: cache failures no longer convert a successful job into a failed one (which caused a retried job to call the external service twice — the single most expensive failure mode a circuit breaker can introduce). Reporter and listener exceptions are contained, a throwing CircuitBreakerOpened listener no longer holds the probe lock until TTL, and a successful probe now closes the circuit even when the counter write fails — that last one alone removed a 91-second recovery penalty on default OldestJobProbe settings.
Residual risks to price in:
release delay instead of counting toward $maxExceptions. This is the right call, but it means a persistently broken cache can produce a long release loop rather than a clean dead-letter — watch queue age, not just failure counts.default_threshold at all hours. Depending on that value, circuits may open earlier than the team's historical baselines.sync (and dispatchSync()) still throws where async releases. Any behavior assertions in tests or any synchronous internal callers need to be exercised on both paths.status_page.enabled off and custom middleware, routes now 404; /fuse/data returns 503 during cache outages. Both are correct, both will trip naive uptime checks.Key questions
default_threshold, and is there guidance for teams that were implicitly relying on the 60-during-peak behavior?fuse.cache.prefix for multiple apps sharing one Redis? The notes only confirm the status page and kill switch.release value, or a way to surface repeated releases as an alertable signal?$tries = 0 on Fuse-gated jobs enforceable or lintable, given a release still counts as an attempt?$this->travel() and Carbon::setTestNow()) have any production implication for long-running workers?Stack fit
Native Laravel idiom throughout: a middleware, a config file, a cache store, a Blade view, an exception-handler-integrated reporter. Fits existing Horizon/supervisor deployments without new infrastructure. The only new runtime dependency is cache availability and headroom, which every Laravel app already has.
Migration path
There is no prior assessment and no prior install, so this is a greenfield adoption of v0.11.0 — but treat the release notes' "Upgrading" section as a checklist anyway, because it enumerates the failure modes we would otherwise rediscover ourselves:
^0.11.0. Do not float across pre-1.0 minors.threshold on each — do not rely on default_threshold.peak_hours_threshold where business-hours behavior is intended.fuse.cache.prefix per app from day one. Accept that history starts empty once.--tag=fuse-views if we customize the status page, so the DATA UNAVAILABLE state is available.$tries = 0 on Fuse-gated jobs, per the README.Compatibility
Laravel/PHP version bounds are not stated in the notes — confirm before scoping. Behavioral compatibility is the real work: threshold resolution, listener exception propagation, gating-error handling, and status-page routing all changed. dispatchSync() and the sync driver diverge from the async path by design; CI must cover both. Existing uptime checks and dashboards pointing at /fuse and /fuse/data need their expected status codes updated (404 when disabled behind custom middleware; 503 during cache outage with the page itself still loading).
Sequencing
Adopt behind a single non-critical service first, with one dependency, in staging, and inject cache failures deliberately (flush the store, add latency, drop connections) to observe: job outcome preservation, release behavior, probe lock release, status page degradation, and recovery on the first good poll. Then roll out per service in ascending order of blast radius, with the payment-adjacent service last. Ship the runbook and the alert changes in the same change window as the config, not after.
Maintenance
Small surface area (middleware + config + cache keys + one view), which keeps maintenance cheap, but pre-1.0 velocity means every minor deserves a read of the changelog. Realistically: one owner, ~1–2 hours per minor release to review and test, plus a config audit whenever threshold or naming semantics change. The Carbon unification is a genuine TCO win — the package's own suite went from 53 seconds to under one, and our integration tests can now advance the open timeout with travel() instead of sleep(), which removes flakiness and wall-clock cost from CI.
Support
Single-maintainer project. Bus factor is the primary support risk; mitigate by keeping our integration thin (middleware + config) so a fork or replacement is a contained exercise, and by tracking the upstream changelog rather than discovering changes at upgrade time. The quality of this changelog — every entry names the failure mode it fixes and the observable symptom — is a positive maturity signal and lowers the cost of that tracking.
Scaling
Every gated dispatch performs cache reads and writes; state churn scales with job volume, not with service count. On Redis this is negligible; on a database cache it becomes a hot table under high throughput and should be avoided. The status page polls independently of job volume. The genuine scaling concern is release storms: when a circuit is open or the cache is erroring, gated jobs release and re-enter, so queue depth and release churn become the load signal to watch, not error rate.
Failure modes
/fuse/data returns 503 while the page still renders DATA UNAVAILABLE in red. Degrades correctly, but release churn rises.CircuitBreakerOpened listener no longer holds the lock until TTL.Ramp-up
Low for any Laravel developer — the concepts map onto middleware and config they already know. The subtle part is not the API but the semantics: a release counts as an attempt, thresholds interact with peak windows, listener exceptions are contained, and sync behaves differently from async. Budget a short internal write-up plus a staging chaos exercise, and put the $tries = 0 rule and the "release is an attempt" caveat in the runbook where on-call will find them.
How can I help you explore Laravel packages today?