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

Laravel Queue Debouncer Laravel Package

mpbarlow/laravel-queue-debouncer

Debounce Laravel queued jobs so rapid repeated dispatches collapse into a single run. This package helps prevent queue flooding and duplicate work by ensuring only the latest job in a debounce window is processed, with simple integration into existing Laravel queues.

View on GitHub
Deep Wiki
Context7

Getting Started

mpbarlow/laravel-queue-debouncer collapses a burst of dispatched jobs into a single execution — the last one wins. It's the queue equivalent of a debounce on an input event, and it's the right tool when a user action fires faster than the work should run (typing in a search box, dragging a slider, saving a model in a tight loop, a webhook provider retrying five times in two seconds).

Minimal setup:

  1. composer require mpbarlow/laravel-queue-debouncer — version 3.2.0 adds Laravel 13 support; the 3.x line otherwise targets Laravel 11/12 with no API changes.
  2. Publish the config if you need to change the window or the cache store: php artisan vendor:publish --tag=queue-debouncer-config. The default window is short; most apps end up overriding it.
  3. Point it at a cache store that supports atomic locks (redis, memcached, database, dynamodb). This is the single most common install failure — see Gotchas.
  4. Add the package's debounce trait (and/or middleware, depending on how your version wires it — check the README example) to the job class, then dispatch the job exactly as you normally would. No changes at the call site.

First use case to try: a RefreshSearchIndex-style job dispatched from a model observer or form request. Dispatch it 20 times in a loop in php artisan tinker (or a test) and confirm your queue only processes one execution, carrying the last payload.

Implementation Patterns

Per-entity debounce keys. The default behaviour debounces all instances of a job class together. For anything model-scoped you want an independent window per record, so make the key stable and specific — typically a method or property on the job that returns something like "refresh-index:{$this->post->id}". Get this wrong and a debounced job for post 1 will silently cancel a pending job for post 2.

Bursty UI → queued side effects. Debounce at the dispatch boundary, not inside the job. Dispatching is cheap; the point is to never enqueue work you'll throw away. This keeps queue depth flat during traffic spikes and is where most of the day-to-day value is.

Webhook and integration fan-in. Providers that retry (Stripe, GitHub, couriers) will happily send you the same event three times in 800ms. Debouncing the downstream sync job gives you idempotency-ish behaviour without building a dedupe table — but only if the payloads are equivalent; see the "last payload wins" gotcha.

Chaining with Bus::batch and other middleware. The debounce middleware composes with WithoutOverlapping and rate limiters, but think about ordering: a debounce that runs before WithoutOverlapping throws away work cheaply, which is what you want. If you're debouncing a job that's part of a batch, note the batch loses any cancelled jobs' callbacks — keep debounced jobs out of batches whose completion logic matters.

Tuning the window per job, not globally. The config default is a floor, not a policy. A 5-second window is right for a search index; it's wrong for a "send the welcome email" job where you want 30–60 seconds to absorb double-submits. Override on the job itself where the version supports it, and leave the global config as a conservative default.

Testing. Test the debounce semantics in a feature test with a real cache driver (array in tests is fine if it supports locks in your Laravel version — verify) and Carbon::setTestNow(). Assert on Queue::size() / dispatched counts rather than job execution. Keep a separate thin test that just asserts the job is dispatchable, because the debounce layer will mask payload bugs if it's the only thing you test.

Observability. Debounced jobs disappear without a trace — that's the feature and the debugging hazard. Log at dispatch time (before the debounce decision) with the debounce key, so when someone asks "why didn't my job run?" you have a trail showing a later dispatch superseded it.

Gotchas and Tips

  • Cache store must support atomic locks. The array store does not share state across processes and older file store implementations didn't lock; if you see the debounce silently not working in production but working in tests, this is almost always why. Set the store explicitly in the package config rather than relying on your app default, which someone will change.
  • Queue::fake() bypasses middleware. Faked queues never run the debounce logic, so a test suite full of Queue::fake() assertions will pass while production debounces everything into one job. Keep at least one integration test that uses a real (sync/array-with-locks) driver.
  • Only the last payload survives. Debouncing is cancellation, not aggregation. If you dispatch SyncOrder(1), SyncOrder(2), SyncOrder(3) under one key, the job runs once with 3 and orders 1 and 2 are never touched. For accumulating work, debounce a "flush" job that reads the full set from the database or cache instead of carrying the payload.
  • Key stability across requests. If the key is derived from an Eloquent model, make sure it's the primary key and not something serialization-dependent. A key that includes spl_object_id or a timestamp means you'll never actually debounce.
  • Clock-sensitive behaviour and time travel. Windows are wall-clock based; Carbon::setTestNow() works, but travel() mid-test can leave a lock held. Prefer asserting "one job ran" over asserting exact timing.
  • Failed jobs and lock release. If the surviving job throws, the lock lifecycle matters on retry — a retried debounced job can be swallowed by a subsequent dispatch in the same window. For jobs where failure must not be lost, pair the debounce with a durable record (a sync_status column) rather than relying on queue retries.
  • Multi-tenant cache prefixes. If your cache prefix varies per tenant and the debounce key doesn't, tenants will cancel each other's work. Conversely, if the prefix varies but the worker's does too, nothing debounces at all.
  • config:cache after config changes. Obvious, universally forgotten, and produces a confusing "my window change did nothing" bug report.
  • Octane and long-running workers. Cache lock connections are long-lived; if you swap cache stores at runtime or flush the cache during a deploy, in-flight debounce state goes with it. Acceptable for most uses, worth knowing before you flush Redis to clear a stuck lock.
  • Extension point: the debounce decision is middleware, so it's the natural place to add your own logging or metrics by wrapping or extending it rather than subclassing the job. Keep custom logic there and your job classes stay plain, testable units.

Overall: 3.2.0 is a compatibility release (Laravel 13 support, first contribution from @scybulski) with no breaking changes or new features. If you're upgrading to Laravel 13, bump the constraint and re-run your one real-driver debounce test — the risk is entirely in cache-lock behaviour, not in the package API.

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.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin