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 Notification Channel Throttling Laravel Package

syropian/laravel-notification-channel-throttling

A Laravel package that adds throttling to notification channels, letting you limit how often notifications are sent per channel or notifiable. Configure caps to prevent spam and duplicate alerts while keeping existing notification workflows intact.

View on GitHub
Deep Wiki
Context7

Getting Started

syropian/laravel-notification-channel-throttling is a small, single-purpose package: it lets you wrap any Laravel notification channel so that deliveries to a given notifiable are rate limited (e.g. "at most 1 email per 10 minutes per user"). It is not a queue middleware or a mail transport — it slots into the via() method of a notification, which makes it easy to adopt on one noisy notification without touching the rest of your app.

Minimal path to first use:

  1. composer require syropian/laravel-notification-channel-throttling — the service provider is auto-discovered, so there is nothing to register.
  2. Publish the config if the package ships one (php artisan vendor:publish --tag=notification-channel-throttling) and skim it; the defaults are usually the first thing you want to change (limit + decay window).
  3. Open src/ThrottledChannel.php before writing any code. The whole surface area is one class with a constructor and a send() that delegates to the real channel behind a RateLimiter check — five minutes of reading removes all ambiguity about argument order and units.
  4. First use case: pick your noisiest notification (comment replies, "someone liked your post", digest-style alerts) and wrap only that channel.
use Syropian\NotificationChannelThrottling\ThrottledChannel;

public function via($notifiable): array
{
    return [
        // Confirm exact signature in the README; the idea is
        // (channel, maxAttempts, decaySeconds).
        (new ThrottledChannel('mail'))->withLimit(1, 600),

        'database', // leave the inbox unthrottled
    ];
}

Upgrading from v1.0.2 is a plain composer update; v1.0.3 only widens the framework constraint (illuminate/* for Laravel 13). There are no API changes, so the same code runs on Laravel 11, 12 and 13.

Implementation Patterns

Wrap only the channels that need it. via() returns an array, so throttling is opt-in per channel. A very common shape is: throttle mail and sms, never throttle database (the in-app inbox should still show every event — throttling it just loses data). Decide this explicitly per notification rather than globally.

Centralise the numbers. Hard-coding withLimit(1, 600) in twelve notifications is a maintenance trap. Either put limits in a config file keyed by notification class + channel, or (cleaner) expose a throttleKey() / throttleLimit() method on a base notification class so each notification declares its own policy in one place.

Ordering with queues. The rate limiter is consulted at the point the channel actually sends. If your notifications are queued with delays or retries, the "window" is measured from send time, not dispatch time — a burst of dispatches during a traffic spike can still all land inside one window if the workers drain them slowly. For genuinely bursty sources, pair this with ShouldBeUnique or a RateLimited job middleware so you drop work early instead of at the channel boundary.

Keying. The limiter key is derived from the notifiable's morph identity plus the channel, which is the behaviour you want for "per user, per channel". Two things to verify against your own code: (a) on-demand / anonymous notifiables (Notification::route('mail', …)) key off the route rather than a model, and (b) if the same user should share a budget across several notifications, you need a custom key — sharing a limiter name across notifications is the extension point to look for.

Testing. Freeze time or set a very short decay, because throttling is time-based and flaky tests here are almost always cache state leaking between tests:

protected function setUp(): void
{
    parent::setUp();
    Notification::fake();
    RateLimiter::clear('/* your key */');
}

Assert on the second send being a no-op rather than on a thrown exception — see the gotcha below.

Integration tip. Because it is just a channel, it composes with everything else: Notification::fake() assertions still behave normally for the first (allowed) send, and it works with on-demand notifications, Notifiable traits and multi-channel fan-out without any changes to your notifiable models.

Gotchas and Tips

  • Throttled sends are dropped silently. The package's whole contract is "don't send", so there is no exception to catch. In production this means a notification can vanish with zero trace. If "did we actually tell the user?" matters, log inside a wrapper or listen for the relevant event if the package fires one — check src/ for an event class before assuming there is one.
  • The cache store is the source of truth. The rate limiter uses Laravel's cache. array works in tests but is per-process; if you run multiple queue workers or web nodes, the store must be shared (redis, database, memcached). A misconfigured per-node store makes throttling silently ineffective in exactly the multi-worker setup where you need it most.
  • Units trap. Laravel's own RateLimiter::attempt takes decaySeconds. Some throttle packages take minutes. Read the constructor once, write a test that asserts a send is allowed again after travel(), and stop thinking about it.
  • Config quirks. If a global limit exists in config, it will override or be overridden by per-notification limits depending on precedence — verify which wins. A publish-then-forget config file is the usual source of "why is it 60 when I set 1?".
  • Don't throttle the database channel. It's tempting to apply the wrapper uniformly; it just loses inbox entries and produces "I never got a notification" bug reports that are impossible to reproduce.
  • Laravel 13 note (v1.0.3). The bump is constraint-only. The real upgrade risk is your other packages' illuminate/support constraints pinning you to 12, not this one. Run composer why-not laravel/framework ^13.0 if the resolver fights you.
  • Debugging checklist for "nothing sends". In order: (1) is the cache store shared and reachable? (2) RateLimiter::clear($key) — does it start working? (3) is the notifiable identity the same object/key you expect (morph map aliases change the key!)? (4) is the wrapped channel name string 'mail' vs the FQCN MailChannel::class — the two produce different limiter keys and different results.
  • Extension points to know. The limiter key and the limit/decay pair are the two seams worth wrapping in your own base class; everything else in this package is intentionally thin, and forking a 100-line channel to add bespoke behaviour is often easier than fighting it.
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.
hsluv/hsluv
seampact/codegen
seampact/hooks
seampact/ir
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table
siberfx/flysystem-wasabi
hollyit/token-replace
ippanelcom/laravel-sdk
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support