Currently Available: Need a skilled Software Developer for your next project?
Categories
Laravel

How to Limit Emails Sent per User with Laravel’s RateLimiter

A user who submits requests repeatedly or adds work to the queue can cause a Laravel request handler or background job to send many messages. Laravel’s RateLimiter counts how many times code associated with a key runs during a set period. The key identifies whose actions the limiter counts. Assign each user a separate key, and check that key when your code accepts an email request.

For queued email, check the limit when your code adds a message to the queue. This limits how many emails a user can add to the queue; the provider’s delivery count can be different. The example uses RateLimiter::attempt() to set a maximum for each user and queue an email only when the user has not reached that limit.

Limit mail when you queue it

Include the user’s ID in the key so Laravel can keep a separate count for each account. RateLimiter::attempt() checks the count, runs the supplied function when the user is under the limit, and records the attempt if that function finishes.

use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\RateLimiter;

$user = request()->user();
$key = 'email:user:' . $user->getKey();

$queued = RateLimiter::attempt(
    $key,
    (int) config('mail_limits.per_user.max_attempts'),
    function () use ($user) {
        Mail::to($user->email)->queue(new AccountNotice());

        return true;
    },
    (int) config('mail_limits.per_user.decay_seconds'),
);

if ($queued === false) {
    $retryAfter = RateLimiter::availableIn($key);

    return response()->json([
        'message' => 'Email limit reached.',
        'retry_after_seconds' => $retryAfter,
    ], 429);
}

return response()->json(['message' => 'Email queued.']);

Set mail_limits.per_user.max_attempts to choose how many emails each user can queue, and mail_limits.per_user.decay_seconds to choose how long the limit lasts. The callback returns true after queueing so the caller can distinguish an accepted request from a blocked one. If Laravel throws an exception while adding the email to the queue, the callback does not finish, so attempt() does not record that attempt.

The Laravel per-day rate-limit example shows how to attach a limit to a key. Choose the window deliberately: a duration-based window does not necessarily reset at midnight. If users need a calendar-day allowance, define and test the reset behavior that matches your application’s time zone and policy.

Choose the point where the limit applies

Laravel counts an email when it adds the message to the queue. A queue worker sends it later, and the mail provider can reject it or fail to deliver it. This approach fits a limit on user requests, such as preventing repeated password-reset or account-notice emails.

If the product rule limits send attempts, check the limit in the queued job or notification when it is time to send the email. Then decide what the worker should do when the user has reached the cap: release the job to retry later, or discard it. Simply returning after a blocked check can remove the job without sending the email. Laravel’s route throttle middleware limits incoming HTTP requests; a queued worker needs its own check at the point where it handles the message.

For Laravel notifications, the jamesmills/laravel-notification-rate-limit package uses Laravel’s limiter to keep separate counters for each notification channel and supports email notifications. Check when it applies the limit, and confirm that timing matches your rule: whether the limit applies when you queue a notification or when you send it.

Keep the counter shared across workers

Every web server and queue worker that enforces the same user limit needs to read and update the same counter. Configure Laravel to save rate-limit counters in a shared cache store that all servers and workers can access. Redis is a common choice for production workloads where requests run at the same time. A local file cache does not share counters between separate hosts. The cited Laravel rate-limiting guidance warns that file-based storage can cause race conditions, where simultaneous requests update a counter in conflicting ways.

Include both the email action and the user in each key. For example, email:user:123 separates one user’s email allowance from another’s, while a key based only on IP address can group unrelated users behind an office network or VPN. If user IDs can repeat across tenants in a multi-tenant service, include the tenant ID in the key too.

Keep per-user limits separate from provider limits

A per-user limiter stops an account from generating too many messages. The mail provider’s sending quota applies to the mailer or provider account as a whole, and the per-user limiter does not enforce it. Laravel mail-throttling resources describe provider-level limits as a separate concern, and the laravel-mail-throttle package focuses on throttling by mailer rather than by user.

Use the user-keyed RateLimiter to enforce each account’s limit, and use provider-aware throttling to manage shared sending capacity. The user limit stops one person from using the whole allowance; provider throttling helps keep total traffic within the provider’s limits.

What I'm building

Delegate tasks. Get software.

Give Vroni a GitHub issue, bug report, spec, or rough idea. It reads the repo, plans the change, writes code, runs checks, and works toward a review-ready pull request.

Take a look at vroni.com

Email updates

Usually a new article and a few links I found interesting.

No spam. Unsubscribe with one click.

Leave a Reply

Your email address will not be published. Required fields are marked *