Redis smooth throttling

This might be an edge case and not even complicated, but I thought I'd share it. So, I had this use case recently where I needed to throttle email send out speed, while allowing to control it on admin panel.
However, the difference from the usual case was that you don't enter for example how many emails to allow per minute or second, but you enter over how long the hole process should be done. So, for example, send all ~1k emails over 1 hour.


Luckily, laravel has redis throttling built in. We can utilize it like so:

<?php

Redis::throttle('key')
    ->allow(1000)
    ->every(3600) // 1 hour
    ->then(function () { /* send */ }, function () { /* release */ });

But, here lies the problem. In this case it will send all that 1000 emails as fast as possible. It might take only a couple of minutes. If you have more than 1k emails, the queue will wait for the remaining time, do nothing, and then after 1 hour will send a bunch of emails again as fast as possible. Doing such a burst of emails is a red flag, you might burn your IP and emails will land to spam.

How do we counteract that? By recalculating allow and every to be as low as possible.


<?php

$emailsPerSecond = $emailsToSend / (3600 * $overHours);

if ($emailsPerSecond < 1) {
    $emails = 1;
    $seconds = (int) floor(1 / $emailsPerSecond);
} else {
    $emails = (int) floor($emailsPerSecond);
    $seconds = 1;
}

Redis::throttle('key')
    ->allow($emails)
    ->every($seconds)
    ->then(function () { /* send */ }, function () { /* release */ });

So this way it will send all the emails smoothly over specified period. In case of 1k emails per hour it would result in ~3.6 emails per second.

Just a note before you go, if you use redis throttling for your jobs, don't forget to define retryUntil method in your job, otherwise it will timeout soon and won't be executed


<?php 

/**
* @return Carbon
*/
public function retryUntil(): Carbon
{
    return now()->addDays(2); // amount depends on your project
}

That's all. Here's a SmoothThrottle class in case you find it useful.


<?php

/**
 * Class SmoothThrottle
 */
class SmoothThrottle
{
    /** @var int */
    private $emails;

    /** @var int */
    private $seconds;

    /**
     * SmoothThrottle constructor.
     * @param int $allowed
     * @param int $over hours
     */
    public function __construct(int $allowed, int $over)
    {
        if ($this->isInvalid($allowed, $over)) {
            $this->emails = 0;
            $this->seconds = 1;

            return;
        }

        $emailsPerSecond = $allowed / (3600 * $over);

        if ($emailsPerSecond < 1) {
            $this->emails = 1;
            $this->seconds = (int)floor(1 / $emailsPerSecond);
        } else {
            $this->emails = (int)floor($emailsPerSecond);
            $this->seconds = 1;
        }
    }

    /**
     * @return int
     */
    public function emails(): int
    {
        return $this->emails;
    }

    /**
     * @return int seconds
     */
    public function seconds(): int
    {
        return $this->seconds;
    }

    /**
     * @param int $allowed
     * @param int $over
     * @return bool
     */
    private function isInvalid(int $allowed, int $over): bool
    {
        return !$allowed || !$over;
    }
}

Comments

Popular posts from this blog

Surround yourself with smarter than you

Fast PHPUnit tests